Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,24 @@ Conditions propagate through nested schemas via `of:`.
schema = PersonSchema.new
schema.to_json_schema
# => {
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
# "title" => "PersonSchema",
# "type" => "object",
# "properties" => { ... },
# "required" => [...],
# "additionalProperties" => false
# }

puts schema.to_json # Pretty JSON string
```

`to_json_schema` and `to_json` return pure Draft 2020-12 JSON Schema documents with JSON-compatible string keys. Provider-only envelope fields such as `name`, `schema`, and `strict` are not included.

For migration, the old RubyLLM/provider envelope remains available:

```ruby
schema.to_ruby_llm_schema
# => {
# name: "PersonSchema",
# description: nil,
# schema: {
Expand All @@ -575,8 +593,6 @@ schema.to_json_schema
# strict: true
# }
# }

puts schema.to_json # Pretty JSON string
```

## License
Expand Down
59 changes: 46 additions & 13 deletions lib/ruby_llm/schema/json_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,27 @@
module RubyLLM
class Schema
module JsonOutput
DRAFT_2020_12_SCHEMA = "https://json-schema.org/draft/2020-12/schema"

def to_json_schema
validate! # Validate schema before generating JSON

schema_hash = {
type: "object",
properties: self.class.properties,
required: self.class.required_properties,
additionalProperties: self.class.additional_properties
}
stringify_keys(resolve_runtime_values({
"$schema" => DRAFT_2020_12_SCHEMA,
title: self.class.schema_metadata[:title] || @name,
description: @description || self.class.description
}.compact.merge(build_schema_hash)))
end

schema_hash[:strict] = self.class.strict unless self.class.strict.nil?
alias to_json_schema_document to_json_schema

# Only include $defs if there are definitions
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?

self.class.send(:merge_conditions, schema_hash, self.class)
self.class.send(:merge_core_keywords, schema_hash, self.class)
def to_ruby_llm_schema
validate! # Validate schema before generating JSON

{
name: @name,
description: resolve_runtime_values(@description || self.class.description),
schema: resolve_runtime_values(schema_hash)
schema: resolve_runtime_values(build_schema_hash(include_strict: true))
}
end

Expand All @@ -35,6 +34,25 @@ def to_json(*_args)

private

def build_schema_hash(include_strict: false)
schema_hash = {
type: "object",
properties: self.class.properties,
required: self.class.required_properties,
additionalProperties: self.class.additional_properties
}

schema_hash[:strict] = self.class.strict if include_strict && !self.class.strict.nil?

# Only include $defs if there are definitions
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?

self.class.send(:merge_conditions, schema_hash, self.class)
self.class.send(:merge_core_keywords, schema_hash, self.class)

schema_hash
end

def resolve_runtime_values(value)
case value
when Proc
Expand All @@ -51,6 +69,21 @@ def resolve_runtime_values(value)
def evaluate_runtime_value(value)
value.arity.zero? ? instance_exec(&value) : value.call(self)
end

def stringify_keys(value)
case value
when Hash
value.each_with_object({}) do |(key, nested_value), stringified|
stringified[key.to_s] = stringify_keys(nested_value)
end
when Array
value.map { |nested_value| stringify_keys(nested_value) }
when Symbol
value.to_s
else
value
end
end
end
end
end
24 changes: 12 additions & 12 deletions spec/ruby_llm/schema/entry_points/class_inheritance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

it "derives schema names from constants, instances, and defaults" do
stub_const("NamedSchema", build_schema_class)
expect(NamedSchema.new.to_json_schema[:name]).to eq("NamedSchema")
expect(NamedSchema.new.to_ruby_llm_schema[:name]).to eq("NamedSchema")

expect(base_schema.new.to_json_schema[:name]).to eq("Schema")
expect(base_schema.new("CustomName").to_json_schema[:name]).to eq("CustomName")
expect(base_schema.new.to_ruby_llm_schema[:name]).to eq("Schema")
expect(base_schema.new("CustomName").to_ruby_llm_schema[:name]).to eq("CustomName")
end

it "honours description precedence" do
Expand All @@ -22,18 +22,18 @@
string :title
end

anonymous_output = base_schema.new.to_json_schema
anonymous_output = base_schema.new.to_ruby_llm_schema
expect(anonymous_output[:description]).to be_nil

class_level_output = schema_with_description.new.to_json_schema
class_level_output = schema_with_description.new.to_ruby_llm_schema
expect(class_level_output[:description]).to eq("Class-level description")

instance_override = schema_with_description.new("Test", description: "Instance description").to_json_schema
instance_override = schema_with_description.new("Test", description: "Instance description").to_ruby_llm_schema
expect(instance_override[:description]).to eq("Instance description")
end

it "controls additional properties and strictness" do
default_output = base_schema.new.to_json_schema
default_output = base_schema.new.to_ruby_llm_schema
expect(default_output[:schema][:additionalProperties]).to eq(false)
expect(default_output[:schema][:strict]).to eq(true)

Expand All @@ -43,7 +43,7 @@
string :title
end

configured_output = configured_schema.new.to_json_schema
configured_output = configured_schema.new.to_ruby_llm_schema
expect(configured_output[:schema][:additionalProperties]).to eq(true)
expect(configured_output[:schema][:strict]).to eq(false)
end
Expand All @@ -56,7 +56,7 @@
integer :count, required: false
end

output = configured_schema.new("ConfiguredSchema").to_json_schema
output = configured_schema.new("ConfiguredSchema").to_ruby_llm_schema

expect(output).to include(
name: "ConfiguredSchema",
Expand Down Expand Up @@ -84,7 +84,7 @@
end

expect(RedefinedSchema.required_properties).to eq([:name])
expect(RedefinedSchema.new.to_json_schema[:schema][:required]).to eq([:name])
expect(RedefinedSchema.new.to_ruby_llm_schema[:schema][:required]).to eq([:name])
end

it "returns nil from property declarations" do
Expand All @@ -102,7 +102,7 @@
schema_class.string :name, required: false

expect(schema_class.required_properties).to eq([])
expect(schema_class.new.to_json_schema[:schema][:required]).to eq([])
expect(schema_class.new.to_ruby_llm_schema[:schema][:required]).to eq([])
end
end

Expand Down Expand Up @@ -131,7 +131,7 @@
end

it "supports full-feature schemas" do
json_output = schema_class.new("TestSchema").to_json_schema
json_output = schema_class.new("TestSchema").to_ruby_llm_schema

expect(json_output[:name]).to eq("TestSchema")
expect(json_output[:schema][:additionalProperties]).to eq(true)
Expand Down
20 changes: 10 additions & 10 deletions spec/ruby_llm/schema/entry_points/factory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

it "derives schema names from constants, instances, and defaults" do
stub_const("NamedFactorySchema", build_factory_schema { string :title })
expect(NamedFactorySchema.new.to_json_schema[:name]).to eq("NamedFactorySchema")
expect(NamedFactorySchema.new.to_ruby_llm_schema[:name]).to eq("NamedFactorySchema")

expect(base_schema.new.to_json_schema[:name]).to eq("Schema")
expect(base_schema.new("CustomName").to_json_schema[:name]).to eq("CustomName")
expect(base_schema.new.to_ruby_llm_schema[:name]).to eq("Schema")
expect(base_schema.new("CustomName").to_ruby_llm_schema[:name]).to eq("CustomName")
end

it "honours description precedence" do
Expand All @@ -22,18 +22,18 @@
string :title
end

anonymous_output = base_schema.new.to_json_schema
anonymous_output = base_schema.new.to_ruby_llm_schema
expect(anonymous_output[:description]).to be_nil

class_level_output = schema_with_description.new.to_json_schema
class_level_output = schema_with_description.new.to_ruby_llm_schema
expect(class_level_output[:description]).to eq("Factory description")

instance_override = schema_with_description.new("NamedSchema", description: "Instance description").to_json_schema
instance_override = schema_with_description.new("NamedSchema", description: "Instance description").to_ruby_llm_schema
expect(instance_override[:description]).to eq("Instance description")
end

it "controls additional properties and strictness" do
default_output = base_schema.new.to_json_schema
default_output = base_schema.new.to_ruby_llm_schema
expect(default_output[:schema][:additionalProperties]).to eq(false)
expect(default_output[:schema][:strict]).to eq(true)

Expand All @@ -43,7 +43,7 @@
string :title
end

configured_output = configured_schema.new.to_json_schema
configured_output = configured_schema.new.to_ruby_llm_schema
expect(configured_output[:schema][:additionalProperties]).to eq(true)
expect(configured_output[:schema][:strict]).to eq(false)
end
Expand All @@ -56,7 +56,7 @@
integer :count, required: false
end

output = configured_schema.new("FactoryConfiguredSchema").to_json_schema
output = configured_schema.new("FactoryConfiguredSchema").to_ruby_llm_schema

expect(output).to include(
name: "FactoryConfiguredSchema",
Expand Down Expand Up @@ -100,7 +100,7 @@
end

it "supports full-feature schemas" do
json_output = schema_class.new("FactorySchema").to_json_schema
json_output = schema_class.new("FactorySchema").to_ruby_llm_schema

expect(json_output[:name]).to eq("FactorySchema")
expect(json_output[:schema][:additionalProperties]).to eq(true)
Expand Down
18 changes: 9 additions & 9 deletions spec/ruby_llm/schema/entry_points/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
describe "schema attributes" do
it "derives schema names from parameters and defaults" do
named_schema = build_helper_schema("ProvidedName") { string :title }
expect(named_schema.to_json_schema[:name]).to eq("ProvidedName")
expect(named_schema.to_ruby_llm_schema[:name]).to eq("ProvidedName")

default_schema = build_helper_schema { string :title }
expect(default_schema.to_json_schema[:name]).to eq("Schema")
expect(default_schema.to_ruby_llm_schema[:name]).to eq("Schema")
end

it "honours description precedence" do
default_output = build_helper_schema("TestSchema") { string :title }.to_json_schema
default_output = build_helper_schema("TestSchema") { string :title }.to_ruby_llm_schema
expect(default_output[:description]).to be_nil

block_description = build_helper_schema("TestSchema") do
description "Block description"
string :title
end.to_json_schema
end.to_ruby_llm_schema
expect(block_description[:description]).to eq("Block description")

parameter_override = build_helper_schema(
Expand All @@ -30,20 +30,20 @@
) do
description "Block description"
string :title
end.to_json_schema
end.to_ruby_llm_schema
expect(parameter_override[:description]).to eq("Parameter description")
end

it "controls additional properties and strictness" do
default_output = build_helper_schema { string :title }.to_json_schema
default_output = build_helper_schema { string :title }.to_ruby_llm_schema
expect(default_output[:schema][:additionalProperties]).to eq(false)
expect(default_output[:schema][:strict]).to eq(true)

configured_output = build_helper_schema do
additional_properties true
strict false
string :title
end.to_json_schema
end.to_ruby_llm_schema

expect(configured_output[:schema][:additionalProperties]).to eq(true)
expect(configured_output[:schema][:strict]).to eq(false)
Expand All @@ -57,7 +57,7 @@
additional_properties false
string :title
integer :count, required: false
end.to_json_schema
end.to_ruby_llm_schema

expect(json_output).to include(
name: "HelperConfiguredSchema",
Expand Down Expand Up @@ -99,7 +99,7 @@
string
null
end
end.to_json_schema
end.to_ruby_llm_schema

expect(json_output[:name]).to eq("HelperSchema")
expect(json_output[:description]).to eq("Comprehensive helper schema")
Expand Down
Loading