Structured Output
GoAI can generate type-safe structured responses using Go generics. Define a Go struct, and GoAI auto-generates the JSON Schema, sends it to the model, and parses the response back into your type.
GenerateObject
package main
import (
"context"
"fmt"
"github.com/zendev-sh/goai"
"github.com/zendev-sh/goai/provider/openai"
)
type Sentiment struct {
Text string `json:"text"`
Sentiment string `json:"sentiment" jsonschema:"enum=positive|negative|neutral"`
Confidence float64 `json:"confidence" jsonschema:"description=Confidence score from 0 to 1"`
}
func main() {
model := openai.Chat("gpt-4o")
result, err := goai.GenerateObject[Sentiment](context.Background(), model,
goai.WithPrompt("Analyze the sentiment: I love this product!"),
)
if err != nil {
panic(err)
}
fmt.Printf("%s (%.0f%% confidence)\n", result.Object.Sentiment, result.Object.Confidence*100)
}The type parameter [Sentiment] tells GoAI to:
- Generate a JSON Schema from the struct via
SchemaFrom[Sentiment]() - Send the schema to the model as the required response format
- Parse the JSON response into a
Sentimentvalue
The result is an *ObjectResult[Sentiment] with the parsed object in result.Object.
Struct Tags
GoAI reads two struct tags to build the schema:
json:"name"- field name in JSON (standard Go convention)jsonschema:"description=...,enum=a|b|c"- description and allowed values
type Recipe struct {
Name string `json:"name" jsonschema:"description=Recipe name"`
Ingredients []string `json:"ingredients"`
Steps []string `json:"steps"`
PrepTime int `json:"prep_time" jsonschema:"description=Prep time in minutes"`
Difficulty string `json:"difficulty" jsonschema:"enum=easy|medium|hard"`
}All exported fields are required by default. Use pointer types for optional (nullable) fields:
type Profile struct {
Name string `json:"name"` // required
Email *string `json:"email"` // nullable
Age *int `json:"age"` // nullable
}SchemaFrom
To inspect the generated schema without making an API call:
schema := goai.SchemaFrom[Recipe]()
fmt.Println(string(schema))This returns the json.RawMessage that GoAI sends to the provider. Useful for debugging or logging.
StreamObject
For long structured responses, stream partial objects as they arrive:
type Article struct {
Title string `json:"title"`
Summary string `json:"summary"`
Sections []string `json:"sections"`
Tags []string `json:"tags"`
}
stream, err := goai.StreamObject[Article](ctx, model,
goai.WithPrompt("Write an article about Go concurrency patterns."),
)
if err != nil {
panic(err)
}
for partial := range stream.PartialObjectStream() {
fmt.Printf("\rTitle: %s (%d sections)", partial.Title, len(partial.Sections))
}
fmt.Println()
result, err := stream.Result()
if err != nil {
panic(err)
}
fmt.Printf("Final: %s - %d sections\n", result.Object.Title, len(result.Object.Sections))PartialObjectStream() returns a <-chan *Article that emits progressively populated objects as JSON tokens arrive. Early emissions may have zero-value fields that fill in over time.
After the channel closes, call Result() to get the final validated object.
Explicit Schema
If reflection-based schema generation does not fit your use case, provide a raw JSON Schema directly with WithExplicitSchema:
schema := json.RawMessage(`{
"type": "object",
"properties": {
"answer": {"type": "string"},
"confidence": {"type": "number"}
},
"required": ["answer", "confidence"],
"additionalProperties": false
}`)
type Response struct {
Answer string `json:"answer"`
Confidence float64 `json:"confidence"`
}
result, err := goai.GenerateObject[Response](ctx, model,
goai.WithPrompt("What is 2+2?"),
goai.WithExplicitSchema(schema),
)The explicit schema is sent to the provider as-is, bypassing SchemaFrom[T](). The response is still parsed into the type parameter, so the schema and struct must be compatible.
Use WithSchemaName to change the schema name sent to the provider (default is "response"):
result, err := goai.GenerateObject[Recipe](ctx, model,
goai.WithPrompt("Give me a cookie recipe."),
goai.WithSchemaName("recipe"),
)Tool-Augmented Structured Output
GenerateObject supports tools and multi-step loops in the same way as GenerateText. Pass WithTools and set WithMaxSteps to allow the model to call tools before producing the final structured response:
type WeatherSummary struct {
Location string `json:"location"`
Temperature float64 `json:"temperature"`
Conditions string `json:"conditions"`
}
result, err := goai.GenerateObject[WeatherSummary](ctx, model,
goai.WithPrompt("What is the weather like in Paris right now?"),
goai.WithTools(weatherTool),
goai.WithMaxSteps(5),
)The schema is sent on every step. The model freely interleaves tool calls and eventually produces the final JSON output when it is ready. Structured output is parsed from the step that returns with finish reason "stop".
ObjectResult.Steps
ObjectResult[T].Steps contains a StepResult for each generation step in the loop, in order. The final entry is always the structured output step. This mirrors GenerateResult.Steps from GenerateText and is useful for inspecting tool calls, per-step token usage, and intermediate responses.
result, err := goai.GenerateObject[WeatherSummary](ctx, model, ...)
if err != nil {
panic(err)
}
fmt.Printf("Completed in %d step(s)\n", len(result.Steps))
for _, step := range result.Steps {
fmt.Printf(" step %d: finish=%s tools=%d\n",
step.Number, step.FinishReason, len(step.ToolCalls))
}MaxSteps Exhaustion
If MaxSteps is reached before the model produces a structured output step (finish reason "stop"), GenerateObject returns an error:
goai: max steps (5) reached without producing structured outputIncrease MaxSteps, simplify the task, or reduce the number of tools to avoid this.
StreamObject is Single-Step
StreamObject does not support tool loops. It issues one streaming request and returns immediately — tool calls would require round-trips that cannot be interleaved with an in-progress stream. If you need tools alongside structured output, use GenerateObject with WithMaxSteps.
Provider Compatibility
Structured output works with any provider that supports JSON Schema response format. Tested with OpenAI, Anthropic, and Google. Other OpenAI-compatible providers generally support it for models that accept JSON Schema.