Structured Outputs Guide

View Source

Overview

Structured outputs allow you to get JSON responses from Gemini models that strictly adhere to a predefined JSON Schema. This feature is essential for:

  • Building reliable APIs that return predictable data structures
  • Extracting structured information from unstructured text
  • Ensuring type safety in downstream processing
  • Integrating with typed languages and strict validation systems

Quick Start

alias Gemini.Types.GenerationConfig

# Define your schema
schema = %{
  "type" => "object",
  "properties" => %{
    "answer" => %{"type" => "string"},
    "confidence" => %{"type" => "number"}
  },
  "required" => ["answer"]
}

# Use the convenient helper
config = GenerationConfig.structured_json(schema)

{:ok, response} = Gemini.generate(
  "What is the capital of France?",
  model: "gemini-2.5-flash",
  generation_config: config
)

{:ok, text} = Gemini.extract_text(response)
{:ok, data} = Jason.decode(text)
# => %{"answer" => "Paris", "confidence" => 0.95}

New Features (November 2025)

The November 2025 Gemini API update added support for advanced JSON Schema keywords:

1. Union Types with anyOf

schema = %{
  "type" => "object",
  "properties" => %{
    "status" => %{
      "anyOf" => [
        %{
          "type" => "object",
          "properties" => %{"success" => %{"type" => "string"}}
        },
        %{
          "type" => "object",
          "properties" => %{"error" => %{"type" => "string"}}
        }
      ]
    }
  }
}

2. Recursive Schemas with $ref

schema = %{
  "$defs" => %{
    "Node" => %{
      "type" => "object",
      "properties" => %{
        "value" => %{"type" => "string"},
        "children" => %{
          "type" => "array",
          "items" => %{"$ref" => "#/$defs/Node"}
        }
      }
    }
  },
  "type" => "object",
  "properties" => %{
    "tree" => %{"$ref" => "#/$defs/Node"}
  }
}

3. Numeric Constraints

schema = %{
  "type" => "object",
  "properties" => %{
    "score" => %{
      "type" => "number",
      "minimum" => 0.0,
      "maximum" => 100.0
    },
    "age" => %{
      "type" => "integer",
      "minimum" => 0,
      "maximum" => 150
    }
  }
}

4. Control Over Additional Properties

# Strict mode - no extra properties allowed
schema = %{
  "type" => "object",
  "properties" => %{
    "name" => %{"type" => "string"}
  },
  "additionalProperties" => false,
  "required" => ["name"]
}

# Allow extra properties with specific type
schema = %{
  "type" => "object",
  "properties" => %{
    "name" => %{"type" => "string"}
  },
  "additionalProperties" => %{"type" => "string"}
}

5. Nullable Fields

schema = %{
  "type" => "object",
  "properties" => %{
    "middleName" => %{
      "type" => ["string", "null"]
    }
  }
}

6. Tuple-like Arrays with prefixItems

# Define a 2D point as [x, y]
schema = %{
  "type" => "object",
  "properties" => %{
    "coordinates" => %{
      "type" => "array",
      "prefixItems" => [
        %{"type" => "number"},
        %{"type" => "number"}
      ],
      "items" => false  # No additional items allowed
    }
  }
}

Model-Specific Considerations

These models automatically preserve the order of properties in your schema:

schema = %{
  "type" => "object",
  "properties" => %{
    "firstName" => %{"type" => "string"},
    "lastName" => %{"type" => "string"},
    "age" => %{"type" => "integer"}
  }
}

config = GenerationConfig.structured_json(schema)
# Output will have properties in order: firstName, lastName, age

Gemini 2.0 Models

These models require explicit property ordering:

schema = %{
  "type" => "object",
  "properties" => %{
    "firstName" => %{"type" => "string"},
    "lastName" => %{"type" => "string"},
    "age" => %{"type" => "integer"}
  }
}

config =
  GenerationConfig.structured_json(schema)
  |> GenerationConfig.property_ordering(["firstName", "lastName", "age"])

Common Patterns

Extract Multiple Entities

schema = %{
  "type" => "object",
  "properties" => %{
    "people" => %{
      "type" => "array",
      "items" => %{
        "type" => "object",
        "properties" => %{
          "name" => %{"type" => "string"},
          "role" => %{"type" => "string"}
        }
      }
    }
  }
}

config = GenerationConfig.structured_json(schema)

{:ok, response} = Gemini.generate(
  "Extract all people mentioned: John is the CEO and Sarah is the CTO.",
  model: "gemini-2.5-flash",
  generation_config: config
)

Sentiment Analysis

schema = %{
  "type" => "object",
  "properties" => %{
    "sentiment" => %{
      "type" => "string",
      "enum" => ["positive", "negative", "neutral"]
    },
    "confidence" => %{
      "type" => "number",
      "minimum" => 0.0,
      "maximum" => 1.0
    },
    "keywords" => %{
      "type" => "array",
      "items" => %{"type" => "string"}
    }
  },
  "required" => ["sentiment", "confidence"]
}

Data Validation and Classification

schema = %{
  "type" => "object",
  "properties" => %{
    "isValid" => %{"type" => "boolean"},
    "category" => %{
      "type" => "string",
      "enum" => ["bug", "feature", "question", "documentation"]
    },
    "priority" => %{
      "type" => "string",
      "enum" => ["low", "medium", "high", "critical"]
    }
  },
  "required" => ["isValid", "category", "priority"]
}

Best Practices

1. Use Required Fields

Always specify which fields are required to ensure complete responses:

schema = %{
  "type" => "object",
  "properties" => %{
    "answer" => %{"type" => "string"},
    "sources" => %{"type" => "array", "items" => %{"type" => "string"}}
  },
  "required" => ["answer"]  # sources is optional
}

2. Add Descriptions for Complex Schemas

Help the model understand your schema intent:

schema = %{
  "type" => "object",
  "description" => "Product information extracted from text",
  "properties" => %{
    "name" => %{
      "type" => "string",
      "description" => "The product name"
    },
    "price" => %{
      "type" => "number",
      "description" => "Price in USD",
      "minimum" => 0
    }
  }
}

3. Use Enums for Classification

Constrain outputs to known values:

schema = %{
  "type" => "object",
  "properties" => %{
    "language" => %{
      "type" => "string",
      "enum" => ["elixir", "python", "javascript", "rust", "go"]
    }
  }
}

4. Combine with Other Generation Config Options

config =
  GenerationConfig.structured_json(schema)
  |> GenerationConfig.temperature(0.1)  # Lower temperature for consistency
  |> GenerationConfig.max_tokens(1000)

Streaming Support

Structured outputs work with streaming responses. Each chunk will contain valid partial JSON:

schema = %{
  "type" => "object",
  "properties" => %{
    "story" => %{"type" => "string"}
  }
}

config = GenerationConfig.structured_json(schema)

{:ok, stream} = Gemini.stream_generate(
  "Write a short story",
  model: "gemini-2.5-flash",
  generation_config: config
)

full_text =
  stream
  |> Enum.map(fn resp ->
    {:ok, text} = Gemini.extract_text(resp)
    text
  end)
  |> Enum.join()

{:ok, data} = Jason.decode(full_text)

Troubleshooting

Schema Validation Errors

If you get schema validation errors, ensure:

  • All required JSON Schema fields are present
  • Types are correctly specified
  • Enums contain at least one value
  • References ($ref) point to valid definitions

Empty or Malformed Responses

If responses don't match your schema:

  • Check that your prompt clearly describes what you want
  • Simplify complex schemas for testing
  • Verify the model supports the schema keywords you're using
  • Try lowering the temperature for more consistent results

Property Ordering Issues (Gemini 2.0)

If property order is incorrect on Gemini 2.0 models:

  • Use property_ordering/2 to explicitly set the order
  • Ensure the ordering list matches all properties in your schema
  • Upgrade to Gemini 2.5+ for automatic ordering

Examples

See the examples/ directory for working code:

  • structured_outputs_basic.exs - Simple structured output example
  • structured_outputs_standalone.exs - Standalone example with Mix.install

Further Reading