Skip to content

Structured Output ​

Control JSON responses from AI models with json_object (simple) or json_schema (validated).

Default: agents return plain text or markdown. Use response_format for JSON output. See Messages for general prompt parameters and Agents for the complete agent lifecycle.

Response Format Types ​

Two JSON response formats:

  • json_object - Valid JSON without schema enforcement
  • json_schema - Schema-validated JSON output

Provider Support ​

Providerjson_objectjson_schemaNotes
OpenAI🟩🟩Native support with strict mode (Responses API only for json_schema)
Anthropic🟦🟩json_object emulated; json_schema native via output_config.format on supported Claude models
OpenRouter🟩🟩Native support, depends on underlying model
Ollama🟨🟨Model-dependent, support varies by model
RubyLLM🟥🟨json_schema depends on the underlying provider/model; ruby_llm has no JSON object mode
Mock🟩🟩Accepted but not validated or enforced

JSON Object Mode ​

Valid JSON without strict schema validation.

Basic Usage ​

ruby
class DataAgent < ApplicationAgent
  generate_with :openai, model: "gpt-4o"

  def extract
    prompt(
      "Extract user info as a JSON object: John Doe, 30, john@example.com",
      response_format: :json_object
    )
  end
end
ruby
response = DataAgent.extract.generate_now
data = response.message.parsed_json
# => { name: "John Doe", age: 30, email: "john@example.com" }

Parsing JSON Objects ​

Use .parsed_json (or aliases .json_object / .parse_json) to extract and parse JSON from responses:

ruby
data = response.message.parsed_json(
  symbolize_names: true,       # Convert keys to symbols (default: true)
  normalize_names: :underscore # Normalize keys (default: :underscore)
)

The method automatically:

  • Finds the first { or [ in the content
  • Extracts JSON between opening and closing brackets
  • Parses and transforms keys as specified
  • Returns nil if parsing fails

Emulated Support (Anthropic) ​

Anthropic doesn't natively support JSON mode. On models that accept a prefilled response (Claude Haiku 4.5, Sonnet 4.5, Opus 4.5 and earlier), with thinking off, ActiveAgent emulates it by:

  1. Prepending "Here is the JSON requested:\n{" to prime Claude
  2. Receiving Claude's continuation
  3. Reconstructing complete JSON
  4. Removing the lead-in from message history

Later models refuse that lead-in with or without thinking, and every model refuses it while thinking is on, so those requests are sent without it and the JSON is read from Claude's answer. See Emulated JSON Object Support.

ruby
class AnthropicAgent < ApplicationAgent
  generate_with :anthropic, model: "claude-haiku-4-5"

  def extract
    prompt(
      "Extract user data as JSON: Jane Smith, jane@example.com, 28",
      response_format: :json_object
    )
  end
end

Best practices for emulated mode:

  • Be explicit in prompts: "return a JSON object"
  • Describe expected structure
  • Validate output in production code

JSON Schema Mode ​

Guaranteed schema conformance with automatic validation.

Using Schema Views ​

Reference schema files from your agent's view directory:

ruby
class DataExtractionAgent < ApplicationAgent
  generate_with :openai, model: "gpt-4o"

  def parse_resume
    prompt(
      message: "Extract resume data: #{params[:file_data]}",
      # Loads views/agents/data_extraction/parse_resume/schema.json
      response_format: :json_schema
    )
  end
end
json
{
  "name": "resume_schema",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "email": { "type": "string" },
      "experience": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "jobTitle": { "type": "string" },
            "company": { "type": "string" },
            "duration": { "type": "string" }
          },
          "required": ["jobTitle", "company", "duration"],
          "additionalProperties": false
        }
      }
    },
    "required": ["name", "email", "experience"],
    "additionalProperties": false
  }
}

Schema Loading ​

Schemas are loaded from standard view paths as {action_name}.json:

  1. Action-specific: views/agents/{agent}/{action}.json
  2. Custom named: views/agents/{agent}/{custom_name}.json

When response_format: :json_schema, it loads {action_name}.json by default.

Named Schemas ​

Share schemas across multiple actions by referencing schema files by name:

ruby
prompt(
  "Extract colors: red, blue, green. Return as json.",
  response_format: {
    type:        "json_schema",
    json_schema: :colors
  }
)

Place shared schemas at the agent level (e.g., views/agents/my_agent/colors.json) and reference them from any action. Use this pattern for:

  • Reusing schemas across multiple actions in the same agent
  • Organizing related schemas in one location
  • Maintaining consistency across agent methods

Inline Schema Definition ​

Pass schemas directly via response_format:

json
prompt(
  "Extract colors: red, blue, green. Return as json.",
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "color_list",
      schema: {
        type: "object",
        properties: {
          colors: {
            type: "array",
            items: { type: "string" }
          }
        },
        required: [ "colors" ],
        additionalProperties: false
      },
      strict: true
    }
  }
)

The name, schema and strict fields go under json_schema, as above, or beside type. Both shapes work with every provider that supports json_schema, so a response_format keeps working when you change providers:

ruby
response_format: { type: "json_schema", name: "user", schema: { type: "object", properties: { name: { type: "string" } } }, strict: true }

The nested json_schema: form is the one used throughout these docs, and the one {action}.json views produce.

Schema Generation ​

Generate schemas from Ruby models for consistency and reusability.

From ActiveModel ​

ruby
class User
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveAgent::SchemaGenerator

  attribute :name, :string
  attribute :email, :string
  attribute :age, :integer

  validates :name, presence: true, length: { minimum: 2 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age, numericality: { greater_than_or_equal_to: 18 }
end
ruby
json_schema = User.to_json_schema
# => { type: "object", properties: { name: {...}, email: {...} }, required: [...] }

From ActiveRecord ​

ruby
class BlogPost < ApplicationRecord
  include ActiveAgent::SchemaGenerator
end

schema = BlogPost.to_json_schema(
  strict: true,
  name: "blog_post",
  exclude: [:created_at, :updated_at]  # Omit timestamps
)

Columns and validations are automatically detected.

Using Generated Schemas ​

Integrate generated schemas into agents:

ruby
prompt(
  message: params[:text],
  response_format: {
    type:        "json_schema",
    json_schema: User.to_json_schema(strict: true, name: "user_data")
  }
)

Troubleshooting ​

Invalid JSON - Check provider support table above. Verify model compatibility and valid JSON Schema.

Missing fields - Use strict: true mode. Add validations to your model.

Type mismatches - Match schema types to provider capabilities. Test with actual responses.

See Also ​