Schema & Structured Output

JSON Schema DSL for the ask-rb ecosystem. Used by tool parameters AND structured LLM output. Zero external dependencies.

gem "ask-schema"

Quick Start

require "ask-schema"

schema = Ask::Schema.create do
  string :name, description: "The person's name"
  integer :age, description: "Their age"
  string :role, description: "Access level", enum: %w[admin user guest]
end

schema.new("person").to_json_schema
# => {name: "person",
#  description: nil,
#  schema:
#   {type: "object",
#    properties:
#     {name: {type: "string", description: "The person's name"},
#      age: {type: "integer", description: "Their age"},
#      role:
#       {type: "string",
#        enum: ["admin", "user", "guest"],
#        description: "Access level"}},
#    required: [:name, :age, :role],
#    additionalProperties: false,
#    strict: true}}

Ask::Schema.create returns a schema class. You instantiate it with a name (schema.new("person")) and call to_json_schema or to_json on the instance.

Basic Types

Ask::Schema.create do
  string  :name                          # string
  integer :count                         # integer
  number  :price, minimum: 0             # number (float)
  boolean :active                        # boolean
  string  :status, enum: %w[pending active]  # string with enum
  array   :tags, of: :string             # array of strings
  null    :deleted_at                    # nullable
end

Each type accepts the usual JSON Schema constraints as keywords: minimum, maximum, pattern, format, min_length, max_length, min_items, max_items, and so on.

Nested Schemas

require "ask-schema"

schema = Ask::Schema.create do
  string :title

  object :author do
    string :name
    string :email
  end

  array :comments do
    object do
      string :text
      string :author
    end
  end
end

schema.new("post").to_json_schema
# => {name: "post",
#  description: nil,
#  schema:
#   {type: "object",
#    properties:
#     {title: {type: "string"},
#      author:
#       {type: "object",
#        properties: {name: {type: "string"}, email: {type: "string"}},
#        required: [:name, :email],
#        additionalProperties: false},
#      comments:
#       {type: "array",
#        items:
#         {type: "object",
#          properties: {text: {type: "string"}, author: {type: "string"}},
#          required: [:text, :author],
#          additionalProperties: false}}},
#    required: [:title, :author, :comments],
#    additionalProperties: false,
#    strict: true}}

Optional and Nullable Fields

By default, all fields are required. Make a field optional with required: false:

require "ask-schema"

schema = Ask::Schema.create do
  string :name
  string :nickname, required: false
  integer :age, required: false
end

schema.new("person").to_json_schema
# => {name: "person",
#  description: nil,
#  schema:
#   {type: "object",
#    properties:
#     {name: {type: "string"},
#      nickname: {type: "string"},
#      age: {type: "integer"}},
#    required: [:name],
#    additionalProperties: false,
#    strict: true}}

Or allow null explicitly with optional:

require "ask-schema"

schema = Ask::Schema.create do
  string :name
  optional :nickname do
    string
  end
end

schema.new("user").to_json_schema
# => {name: "user",
#  description: nil,
#  schema:
#   {type: "object",
#    properties:
#     {name: {type: "string"},
#      nickname: {anyOf: [{type: "string"}, {type: "null"}]}},
#    required: [:name, :nickname],
#    additionalProperties: false,
#    strict: true}}

Reusable Definitions

define creates a named sub-schema; reference it with of: — the output gets proper $defs and $ref:

require "ask-schema"

class User < Ask::Schema
  define(:address) do
    string :street
    string :city
    string :zip
  end

  string :name
  object :home_address, of: :address
  object :work_address, of: :address
end

User.properties[:home_address]
# => {"$ref" => "#/$defs/address"}

Conditionals

Use given for if/then/else-style branches. Values coerce automatically: scalars become const, arrays become enum, regexps become pattern:

require "ask-schema"

schema = Ask::Schema.create do
  integer :age
  string :country

  given(age: 18, country: "US") do
    requires :license_number
    validates :license_number, type: :string, pattern: /^[A-Z]{2}\d{6}$/
    otherwise do
      requires :country_name
    end
  end
end

json = schema.new("form").to_json_schema
json.dig(:schema, :if)
# => {properties: {"age" => {const: 18}, "country" => {const: "US"}}, required:
# ["age", "country"]}
json.dig(:schema, :then, :required)
# => ["license_number"]
json.dig(:schema, :else, :required)
# => ["country_name"]

dependent requires fields whenever another field is present:

require "ask-schema"

schema = Ask::Schema.create do
  string :shipping_address
  dependent :shipping_address do
    requires :name, :street, :city
  end
end

schema.new("order").to_json_schema
# => {name: "order",
#  description: nil,
#  schema:
#   {type: "object",
#    properties: {shipping_address: {type: "string"}},
#    required: [:shipping_address],
#    additionalProperties: false,
#    strict: true,
#    dependentRequired: {"shipping_address" => ["name", "street", "city"]}}}

Using with Tools

The schema DSL powers tool parameter definitions in ask-tools:

require "ask-tools"

class SearchTool < Ask::Tool
  description "Search the knowledge base"

  param :query, type: :string, desc: "Search query", required: true
  param :limit, type: :integer, desc: "Max results", required: false

  def execute(query:, limit: 10)
    # ...
  end
end

SearchTool.params_schema
# => {type: "object",
#  properties:
#   {"query" => {type: "string", description: "Search query"},
#    "limit" => {type: "integer", description: "Max results"}},
#  required: ["query"],
#  additionalProperties: false}

Each param declaration generates a JSON Schema entry in params_schema.

Using with Structured Output

Pass a schema to get structured JSON back from the LLM:

schema = Ask::Schema.create do
  string :name
  integer :age
  array  :hobbies, of: :string
end

response = provider.chat(
  [{ role: "user", content: "Tell me about John, 28, who likes hiking and photography" }],
  model: "deepseek-v4-flash",
  schema: schema.new("person")
)

JSON.parse(response.content)
# => { "name" => "John", "age" => 28, "hobbies" => ["hiking", "photography"] }

Providers serialize the schema instance via to_json_schema into their own structured-output format.

Validation

Schemas validate themselves at definition time. Circular references are detected and rejected:

require "ask-schema"

schema = Ask::Schema.create { string :name }
schema.valid?     # => true
schema.validate!  # => nil

Implementation

  • Zero dependencies — pure Ruby, no JSON Schema gems required
  • Strict by defaultstrict true and additional_properties false are the defaults; override per-class
  • Thread-safe — stateless definition blocks
  • Standalone — works without any other ask-rb gem

Next Steps


This site uses Just the Docs, a documentation theme for Jekyll.