Schematist JSON Schema DSL for Ruby

repository·main·Indexed 40 days ago

https://github.com/crmne/ruby_llm-schema

A general-purpose JSON Schema DSL for Ruby that provides a Rails-inspired API for defining structured data. It emits Draft 2020-12 schemas and is designed for RubyLLM workflows, including structured LLM outputs and tool parameters. Formerly known as ruby_llm-schema.

Tokens
12.4K
Snippets
38
Records
49
Agent score
48%

What's inside Schematist

  1. Use Runtime Values in Schemas

    main

    Schema values can be proc objects that are resolved when the schema is rendered. This allows for dynamic schemas (e.g., enums populated from a database).

    • Proc with no arguments: Evaluated in the instance context; can access instance variables.
    • Proc with one argument: Receives the schema instance as an argument.
    class RoleSchema < Schematist::Schema
      description -> { "Roles available to #{@account.name}" }
    
      string :role, enum: -> { @account.roles.pluck(:name) }
    
      def initialize(account:)
        super()
        @account = account
      end
    end
    
    RoleSchema.new(account: account).to_json_schema
  2. Define JSON Schemas with Schematist

    main

    Schematist provides a Rails-inspired DSL for defining JSON Schema (Draft 2020-12) in Ruby. You can define schemas using three different patterns: Class Inheritance, a Factory Method, or a Global Helper.

    Class Inheritance

    Inherit from Schematist::Schema to define a schema class.

    Factory Method

    Use Schematist::Schema.create to define a schema using a block.

    Global Helper

    Include Schematist::Helpers to use the schema method directly.

    # Class Inheritance
    class PersonSchema < Schematist::Schema
      string :name, description: "Person's full name"
      number :age
      boolean :active, required: false
    
      object :address do
        string :street
        string :city
      end
    
      array :tags, of: :string
    end
    
    schema = PersonSchema.new
    puts schema.to_json
    
    # Factory Method
    PersonSchema = Schematist::Schema.create do
      string :name
      number :age
    end
    
    # Global Helper
    require 'schematist'
    include Schematist::Helpers
    
    person_schema = schema "PersonData", description: "A person object" do
      string :name
      number :age
    end
  3. Install Schematist

    main

    To use Schematist in your Ruby application, add the gem to your Gemfile and run bundle install, or install it directly via the command line.

    Note: This gem was formerly known as ruby_llm-schema.

    # In your Gemfile
    gem 'schematist'
    bundle install
    # OR
    gem install schematist
  4. Define Sub-schemas and References

    main

    Schema Definitions

    You can use define to create reusable sub-schemas within a class or define block. Definitions can be any schema type.

    References

    To reuse schemas, you can:

    • Use the of: option in arrays or objects.
    • Use the reference: option in objects.
    • Use the reference method inside an object block.
    • Reference the root schema using reference: :root.

    Nested Schemas

    You can embed existing Schematist::Schema classes directly within objects or arrays for composition.

    class MySchema < Schematist::Schema
      define :location do
        string :latitude
        string :longitude
      end
    
      array :coordinates, of: :location
      object :home_location, reference: :location
    
      object :user do
        reference :location
      end
    
      object :ui_schema do
        string :element, enum: ["input", "button"]
        string :label
        object :sub_schema, reference: :root
      end
    end
    
    # Embedding classes
    class PersonSchema < Schematist::Schema
      string :name
      integer :age
    end
    
    class CompanySchema < Schematist::Schema
      object :ceo, of: PersonSchema
      array :employees, of: PersonSchema
      object :founder do
        PersonSchema.new
      end
    end
  5. Define schema property types and annotations

    main

    A schema is a collection of properties. Every property requires a name and can optionally include a description and a required flag (which defaults to true).

    Annotations

    Annotations provide metadata for humans and tools (like title, description, default, examples, deprecated, read_only, and write_only) but do not affect validation.

    • Short annotations: Passed as keyword arguments to the property method.
    • Long annotations: Defined inside a block to annotate the enclosing schema (e.g., an object).

    If the same annotation is provided both as a keyword and inside a block, the keyword argument takes precedence.

    string :name, description: "Person's full name"
    number :age, description: "Person's age", required: false
    boolean :is_active, description: "Whether the person is active"
    null :placeholder, description: "A placeholder property"
    
    # Annotations as keywords
    string :email, 
      title: "Email address", 
      description: "Primary contact email", 
      default: "user@example.com", 
      examples: ["alice@example.com"]
    
    # Annotations in a block (annotating the object)
    object :account do
      title "Account"
      description "Billing account metadata used for invoices."
      examples [{ id: "acct_123", status: "active" }]
    
      string :id
      string :status
    end
  6. Migrate from ruby_llm-schema to Schematist

    main

    If you are upgrading from the deprecated ruby_llm-schema gem to schematist, follow these steps:

    1. Update Gemfile: Replace gem 'ruby_llm-schema' with gem 'schematist'.
    2. Update Class Inheritance: Change RubyLLM::Schema to Schematist::Schema.
    3. Update Helpers: Change include RubyLLM::Helpers to include Schematist::Helpers.
    4. Update Error Names: RubyLLM::Schema::ValidationError is now Schematist::ValidationError.
    5. Handle strict flag: The strict option has been removed from the schema object. You must now set this flag manually when building your provider request (e.g., in an OpenAI response_format payload).
    # Gemfile
    gem 'schematist'                      # was: gem 'ruby_llm-schema'
    
    # Schema Definition
    class PersonSchema < Schematist::Schema   # was: RubyLLM::Schema
    end
    
    # Using Helpers
    include Schematist::Helpers               # was: RubyLLM::Helpers
  7. Define parameters for RubyLLM tools

    main

    RubyLLM tools can use Schematist schemas to define their expected parameters. This allows you to reuse argument shapes across different tools or parts of your application.

    There are two ways to define parameters in a RubyLLM::Tool:

    1. Shared Schema: Pass a Schematist schema class to the parameters method.
    2. Inline Schema: Use a block with parameters do ... end to define the schema directly within the tool class.
    # Using a shared schema class
    class SearchParams < Schematist::Schema
      string :query, description: "Search query"
      integer :limit, required: false, description: "Maximum results"
    end
    
    class SearchDocuments < RubyLLM::Tool
      description "Searches internal documents"
      parameters SearchParams
    
      def execute(query:, limit: 10)
        DocumentSearch.call(query:, limit:)
      end
    end
    
    # Using an inline schema block
    class Weather < RubyLLM::Tool
      description "Gets current weather"
    
      parameters do
        string :city, description: "City name"
        string :units, enum: %w[celsius fahrenheit], required: false
      end
    
      def execute(city:, units: "celsius")
        WeatherAPI.current(city:, units:)
      end
    end
  8. Migrate from the provider envelope to direct JSON Schema

    main

    In previous versions, to_json_schema returned an envelope object (e.g., {name:, description:, schema:, strict:}). In Schematist, to_json_schema returns the raw schema document directly.

    Key Changes:

    • No more digging: Do not access [:schema] to get the document. Access properties directly from the returned hash.
    • String keys: The returned hash uses string keys, not symbols.
    • Manual Envelope Construction: If your LLM provider requires an envelope (like OpenAI's response_format), you must construct it yourself.
    # BEFORE (digging into the envelope)
    schema.to_json_schema[:schema][:properties]
    
    # NOW (direct access with string keys)
    schema.to_json_schema["properties"]
    
    # Constructing an envelope for a provider manually
    {
      name: "PersonSchema",
      schema: PersonSchema.new.to_json_schema,
      strict: true
    }
  9. Build conditional schema branches with ConditionalBuilder

    main

    The ConditionalBuilder is used within the Schematist DSL to define branches for conditional logic (such as if, then, or else blocks). Each branch is backed by its own schema class, meaning you can use the full suite of standard schema DSL features—including nested objects, arrays, composition, and references—inside a branch.

    To simplify common tasks, ConditionalBuilder provides two shorthand methods:

    1. requires(*fields): Specifies that certain properties must be present without adding further constraints to them.
    2. validates(field, **options): Adds specific constraints to a field.

    If a branch only uses requires and no other validations, it is optimized into a dependentRequired schema rather than a full dependentSchemas block.

  10. Set a self-schema for primitive types

    main

    When a type is called without a name (e.g., string instead of string :name), it represents a schema that is the type itself. You can explicitly set this using self_schema(definition). This is useful when building complex schemas that compose primitive types directly.

    # Example of a schema that IS just a string with specific constraints
    StringSchema = Schematist::Schema.create do
      self_schema string(enum: %w[a b c])
    end
  11. How schema blocks and annotations work

    main

    When using a block with a schema builder (like object_schema or array_schema), the block yields a context that allows you to declare nested schemas and set keywords.

    Schema Block Yields

    1. Nested Schemas: Declared using type methods (e.g., string, integer).
    2. Keywords: Set on the enclosing node (e.g., contains, content_schema).
    3. Annotations: Metadata that describes the schema.

    Precedence

    Annotations set inside a block are treated as defaults. Explicit options passed to the builder method or keyword annotations passed as arguments will override them.

    Special Block Methods

    • contains(min: nil, max: nil, &block): Defines the contains keyword for arrays.
    • any_schema: Injects a true schema (accepts everything).
    • no_schema: Injects a false schema (accepts nothing).
    • raw(schema): Injects a raw schema object.
    • content_schema(&block): Sets the contentSchema keyword.