Skip to main content

Overview

Structured outputs allow you to define schemas for chat completions and receive parsed, type-safe responses. This is perfect for extracting structured data, building forms, or ensuring consistent response formats.

Defining Models

Use OpenAI::BaseModel to define your response structure:
class Location < OpenAI::BaseModel
  required :address, String
  required :city, String, doc: "City name"
  required :postal_code, String, nil?: true
end

class Participant < OpenAI::BaseModel
  required :first_name, String
  required :last_name, String, nil?: true
  required :status, OpenAI::EnumOf[:confirmed, :unconfirmed, :tentative]
end

class CalendarEvent < OpenAI::BaseModel
  required :name, String
  required :date, String
  required :participants, OpenAI::ArrayOf[Participant]
  required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
  required :is_virtual, OpenAI::Boolean
  required :location,
           OpenAI::UnionOf[String, Location],
           nil?: true,
           doc: "Event location"
end

Complete Example

Here’s a full example of using structured outputs to extract calendar event information:
#!/usr/bin/env ruby
# frozen_string_literal: true
# typed: strong

require_relative "../lib/openai"

class Location < OpenAI::BaseModel
  required :address, String
  required :city, String, doc: "City name"
  required :postal_code, String, nil?: true
end

# Participant model with an optional last_name and an enum for status
class Participant < OpenAI::BaseModel
  required :first_name, String
  required :last_name, String, nil?: true
  required :status, OpenAI::EnumOf[:confirmed, :unconfirmed, :tentative]
end

# CalendarEvent model with a list of participants.
class CalendarEvent < OpenAI::BaseModel
  required :name, String
  required :date, String
  required :participants, OpenAI::ArrayOf[Participant]
  required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
  required :is_virtual, OpenAI::Boolean
  required :location,
           OpenAI::UnionOf[String, Location],
           nil?: true,
           doc: "Event location"
end

# gets API Key from environment variable `OPENAI_API_KEY`
client = OpenAI::Client.new

chat_completion = client.chat.completions.create(
  model: "gpt-4o-2024-08-06",
  messages: [
    {role: :system, content: "Extract the event information."},
    {
      role: :user,
      content: <<~CONTENT
        Alice Shah and Lena are going to a science fair on Friday at 123 Main St. in San Diego.
        They have also invited Jasper Vellani and Talia Groves - Jasper has not responded and Talia said she is thinking about it.
      CONTENT
    }
  ],
  response_format: CalendarEvent
)

chat_completion
  .choices
  .reject { _1.message.refusal }
  .each do |choice|
    # parsed is an instance of `CalendarEvent`
    pp(choice.message.parsed)
  end

Model Features

Type Safety

Support for standard Ruby types:
class User < OpenAI::BaseModel
  required :name, String
  required :age, Integer
  required :active, OpenAI::Boolean
end

Optional Fields

Mark fields as nullable with nil?: true:
class Person < OpenAI::BaseModel
  required :first_name, String
  required :last_name, String, nil?: true  # Can be nil
end

Documentation

Add descriptions to help the model understand field purposes:
class Location < OpenAI::BaseModel
  required :city, String, doc: "City name"
  required :postal_code, String, doc: "ZIP or postal code"
end

Accessing Parsed Data

The parsed property gives you a fully typed instance of your model:
chat_completion
  .choices
  .reject { _1.message.refusal }
  .each do |choice|
    # parsed is an instance of `CalendarEvent`
    event = choice.message.parsed
    
    puts event.name
    puts event.date
    event.participants.each do |participant|
      puts "#{participant.first_name} - #{participant.status}"
    end
  end
Always check for refusals before accessing parsed data. The model may refuse to generate a response if the request violates content policies.

Streaming with Structured Outputs

You can combine streaming with structured outputs for real-time parsing:
stream = client.chat.completions.stream(
  model: "gpt-4o-mini",
  response_format: MathResponse,
  messages: [
    {role: :user, content: "solve 8x + 31 = 2, show all steps"}
  ]
)

stream.each do |event|
  case event
  when OpenAI::Streaming::ChatContentDoneEvent
    # Access the parsed object when complete
    pp(event.parsed)
  end
end
See the Streaming example for more details.

Best Practices

1

Use descriptive field names

Clear field names help the model understand what data to extract.
2

Add documentation

Use the doc parameter to provide additional context for complex fields.
3

Handle refusals

Always filter out refusals before accessing parsed data.
4

Use appropriate types

Choose the right type helpers (ArrayOf, EnumOf, UnionOf) for your data structure.

Supported Models

Structured outputs require models that support the feature, such as gpt-4o-2024-08-06 or later. Check the OpenAI documentation for the latest model support.

Next Steps

Function Calling

Learn how to use structured outputs with function calling and tools

Streaming

Stream structured outputs in real-time

Build docs developers (and LLMs) love