Structured Output
Get AI responses that match an exact JSON schema with required fields and types
Table of contents
After reading this guide, you will know:
- How JSON mode differs from schema-validated structured output.
- How to define a schema with
RubyLLM::Schema. - How to provide a manual JSON Schema and name it.
- How to build complex nested object and array schemas.
- Which providers support structured output and how to add or remove a schema mid-conversation.
Getting Structured Output
When building applications, you often need AI responses in a specific format for parsing and processing. RubyLLM provides two approaches: JSON mode for valid JSON output, and structured output for guaranteed schema compliance.
JSON mode (using with_provider_options(response_format: { type: 'json_object' })) guarantees valid JSON but not any specific structure. Structured output (with_schema) guarantees the response matches your exact schema with required fields and types. Use structured output when you need predictable, validated responses.
chat = RubyLLM.chat.with_provider_options(response_format: { type: 'json_object' })
response = chat.ask("List 3 programming languages with their year created. Return as JSON.")
class LanguagesSchema < RubyLLM::Schema
array :languages do
object do
string :name
integer :year
end
end
end
chat = RubyLLM.chat.with_schema(LanguagesSchema)
response = chat.ask("List 3 programming languages with their year created")
# Always returns: {"languages" => [{"name" => "...", "year" => ...}, ...]}
Using RubyLLM::Schema (Recommended)
The easiest way to define schemas is with the RubyLLM::Schema gem:
# First, add to your Gemfile:
# gem 'ruby_llm-schema'
#
# Then in your code:
require 'ruby_llm/schema'
class PersonSchema < RubyLLM::Schema
string :name, description: "Person's full name"
integer :age, description: "Person's age in years"
string :city, required: false, description: "City where they live"
end
chat = RubyLLM.chat
response = chat.with_schema(PersonSchema).ask("Generate a person named Alice who is 30 years old")
puts response.parsed # => {"name" => "Alice", "age" => 30}
puts response.content # => '{"name":"Alice","age":30}'
RubyLLM::Schema classes automatically use their class name (e.g., PersonSchema) as the schema name in API requests, which can help the model better understand the expected output structure.
Using Manual JSON Schemas
If you prefer not to use RubyLLM::Schema, you can provide a JSON Schema directly:
person_schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
hobbies: {
type: 'array',
items: { type: 'string' }
}
},
required: ['name', 'age', 'hobbies'],
additionalProperties: false # Required for OpenAI structured output
}
chat = RubyLLM.chat
response = chat.with_schema(person_schema).ask("Generate a person who likes Ruby")
puts response.parsed
# => {"name" => "Bob", "age" => 25, "hobbies" => ["Ruby programming", "Open source"]}
OpenAI Requirement: When using manual JSON schemas with OpenAI, you must include additionalProperties: false in your schema objects. RubyLLM::Schema handles this automatically.
Custom Schema Names
By default, schemas are named ‘response’ in API requests. You can provide a custom name that can influence model behavior and aid debugging:
person_schema = {
name: 'PersonSchema',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
},
required: ['name', 'age'],
additionalProperties: false
}
}
chat = RubyLLM.chat
response = chat.with_schema(person_schema).ask("Generate a person")
Custom schema names are useful for:
- Influencing model behavior - Descriptive names can help the model better understand the expected output structure
- Debugging and logging - Identifying which schema was used in API requests
Complex Nested Schemas
Structured output supports complex nested objects and arrays:
class CompanySchema < RubyLLM::Schema
string :name, description: "Company name"
array :employees do
object do
string :name
string :role, enum: ["developer", "designer", "manager"]
array :skills, of: :string
end
end
object :metadata do
integer :founded
string :industry
end
end
chat = RubyLLM.chat
response = chat.with_schema(CompanySchema).ask("Generate a small tech startup")
response.parsed["employees"].each do |employee|
puts "#{employee['name']} - #{employee['role']}"
end
Provider Support
Not all models support structured output. Currently supported:
- OpenAI: GPT-4o, GPT-4o-mini, and newer models
- Anthropic: Claude 4.5+ models (Haiku, Sonnet, Opus)
- Gemini: Gemini 1.5 Pro/Flash and newer
Models that don’t support structured output:
chat = RubyLLM.chat(model: 'gpt-3.5-turbo')
chat.with_schema(schema)
response = chat.ask('Generate a person')
# Provider will return an error if unsupported
Multi-turn Conversations with Schemas
You can add or remove schemas during a conversation:
chat = RubyLLM.chat
chat.with_schema(PersonSchema)
person = chat.ask("Generate a person")
# Remove the schema for free-form responses
chat.without_schema
analysis = chat.ask("Tell me about this person's potential career paths")
class CareerPlanSchema < RubyLLM::Schema
string :title
array :steps, of: :string
integer :years_required
end
chat.with_schema(CareerPlanSchema)
career = chat.ask("Now structure a career plan")
puts person.content
puts analysis.content
puts career.content
Next Steps
- Chat - the conversation interface
with_schemabuilds on. - Tools - let the model call your Ruby code when a schema is not enough.
- Rails Integration - persist structured responses alongside your conversations.
- Advanced Request Control - reach JSON mode and other provider-specific options directly.