Generating Structured Data
While text generation can be useful, your use case will likely call for generating structured data. For example, you might want to extract information from text, classify data, or generate synthetic data.
Many language models are capable of generating structured data, often defined as using "JSON modes" or "tools". However, you need to manually provide schemas and then validate the generated data as LLMs can produce incorrect or incomplete structured data.
The Go AI SDK standardizes structured output via the Output option on ai.GenerateText() and ai.StreamText(). Five output factories cover all common patterns:
| Factory | Use case |
|---|---|
ai.TextOutput() | Plain text (default) |
ai.ObjectOutput[T]() | Typed struct from JSON |
ai.ArrayOutput[T]() | Slice of typed structs from JSON |
ai.ChoiceOutput[T]() | Enum/classification |
ai.JSONOutput() | Untyped interface{} JSON |
Note: The older
ai.GenerateObject()andai.StreamObject()functions are deprecated. Use theOutputoption instead.
Structured Output with GenerateText
Use GenerateText with an Output factory to get a fully typed, validated result in a single call. The SDK automatically sets the correct ResponseFormat and parses the model's response.
Object Output
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
)
type Recipe struct {
Name string `json:"name"`
Ingredients []string `json:"ingredients"`
Steps []string `json:"steps"`
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-4o")
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate a lasagna recipe.",
Output: ai.ObjectOutput[Recipe](ai.ObjectOutputOptions{
Schema: ai.SchemaFor[Recipe](),
Name: "recipe",
Description: "A complete recipe with name, ingredients, and steps",
}),
})
if err != nil {
log.Fatal(err)
}
// Type-assert the Output field to get your typed struct
recipe := result.Output.(Recipe)
fmt.Printf("Recipe: %s\n", recipe.Name)
fmt.Printf("Steps: %v\n", recipe.Steps)
}
Array Output
type Hero struct {
Name string `json:"name"`
Class string `json:"class"`
Description string `json:"description"`
}
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Generate 3 hero descriptions for a fantasy RPG.",
Output: ai.ArrayOutput[Hero](ai.ArrayOutputOptions[Hero]{
ElementSchema: ai.SchemaFor[Hero](),
Name: "heroes",
}),
})
if err != nil {
log.Fatal(err)
}
heroes := result.Output.([]Hero)
for _, h := range heroes {
fmt.Printf("%s (%s): %s\n", h.Name, h.Class, h.Description)
}
Choice Output
type Sentiment string
const (
Positive Sentiment = "positive"
Negative Sentiment = "negative"
Neutral Sentiment = "neutral"
)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: `Classify the sentiment of: "I love this product!"`,
Output: ai.ChoiceOutput[Sentiment](ai.ChoiceOutputOptions[Sentiment]{
Options: []Sentiment{Positive, Negative, Neutral},
}),
})
if err != nil {
log.Fatal(err)
}
sentiment := result.Output.(Sentiment)
fmt.Printf("Sentiment: %s\n", sentiment) // positive
JSON Output (Untyped)
result, err := ai.GenerateText(ctx, ai.GenerateTextOptions{
Model: model,
Prompt: "Return some arbitrary structured data about the Go language.",
Output: ai.JSONOutput(ai.JSONOutputOptions{}),
})
if err != nil {
log.Fatal(err)
}
data := result.Output.(map[string]interface{})
fmt.Printf("Data: %v\n", data)
Structured Streaming with StreamText
Use StreamText with an Output factory to receive structured data as it streams. Call result.PartialOutput() at any time to get the most recently parsed partial value.
type Notification struct {
Name string `json:"name"`
Message string `json:"message"`
}
result, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate a system notification.",
Output: ai.ObjectOutput[Notification](ai.ObjectOutputOptions{
Schema: ai.SchemaFor[Notification](),
}),
})
if err != nil {
log.Fatal(err)
}
defer result.Close()
for chunk := range result.Chunks() {
if chunk.Type == provider.ChunkTypeText {
// Read incremental parsed value during streaming
partial := result.PartialOutput()
if n, ok := partial.(Notification); ok {
fmt.Printf("Partial name: %s\n", n.Name)
}
}
}
// Full text after stream completes
fmt.Printf("Raw JSON: %s\n", result.Text())
Element Streaming for Arrays
When using ArrayOutput, you can receive fully validated array elements one at a time as they stream:
streamResult, err := ai.StreamText(ctx, ai.StreamTextOptions{
Model: model,
Prompt: "Generate 5 notification objects.",
Output: arrayOut, // ArrayOutput[Notification]
})
if err != nil {
log.Fatal(err)
}
elements := ai.ElementStreamWithOutput(streamResult, arrayOut)
for elem := range elements {
fmt.Printf("Notification %d: %s\n", elem.Index, elem.Element.Name)
}
// Check for a stream-level error after consuming all elements
if err := streamResult.Err(); err != nil {
log.Printf("Error: %v", err)
}
SchemaFor Helper
ai.SchemaFor[T]() generates a JSON Schema from a Go struct's field types and json tags. Use it instead of writing schemas by hand:
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
schema := ai.SchemaFor[Address]()
// Result: {"type":"object","properties":{"street":{"type":"string"},...},"required":["street","city","country"]}
Fields with omitempty in their json tag become optional (not included in required). Fields tagged json:"-" are excluded.
Legacy API (Deprecated)
Deprecated: The functions below still work but are deprecated. Prefer
GenerateText/StreamTextwithOutputinstead.
Generate Object
The ai.GenerateObject() function generates structured data from a prompt. The schema is also used to validate the generated data, ensuring type safety and correctness.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/digitallysavvy/go-ai/pkg/ai"
"github.com/digitallysavvy/go-ai/pkg/providers/openai"
"github.com/digitallysavvy/go-ai/pkg/schema"
)
type Recipe struct {
Name string `json:"name"`
Ingredients []struct {
Name string `json:"name"`
Amount string `json:"amount"`
} `json:"ingredients"`
Steps []string `json:"steps"`
}
func main() {
ctx := context.Background()
provider := openai.New(openai.Config{APIKey: os.Getenv("OPENAI_API_KEY")})
model, _ := provider.LanguageModel("gpt-4")
// Define JSON Schema
recipeSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"recipe": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"ingredients": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"amount": map[string]interface{}{"type": "string"},
},
},
},
"steps": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
},
},
},
},
"required": []string{"recipe"},
}
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema.NewSimpleJSONSchema(recipeSchema),
Prompt: "Generate a lasagna recipe.",
})
if err != nil {
log.Fatal(err)
}
// result.Object is already unmarshaled; re-marshal it to decode into a typed struct
objBytes, err := json.Marshal(result.Object)
if err != nil {
log.Fatal(err)
}
var recipe Recipe
if err := json.Unmarshal(objBytes, &recipe); err != nil {
log.Fatal(err)
}
fmt.Printf("Recipe: %s\n", recipe.Name)
fmt.Printf("Ingredients: %+v\n", recipe.Ingredients)
fmt.Printf("Steps: %+v\n", recipe.Steps)
}
Accessing Response Headers & Body
Sometimes you need access to the full response from the model provider, e.g. to access some provider-specific headers or body content.
You can access the raw response headers and body using the Response field:
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate a recipe.",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Headers: %+v\n", result.Response.Headers)
fmt.Printf("Body: %+v\n", result.Response.Body)
Stream Object
Given the added complexity of returning structured data, model response time can be unacceptable for your interactive use case. With the ai.StreamObject() function, you can receive partial objects as they are parsed during generation via the OnChunk callback. Unlike StreamText, ai.StreamObject() blocks until generation completes and returns the final *GenerateObjectResult directly — it does not return a channel-based stream.
result, err := ai.StreamObject(ctx, ai.StreamObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate a lasagna recipe.",
OnChunk: func(partialObject interface{}) {
fmt.Printf("Partial object: %v\n", partialObject)
},
})
if err != nil {
log.Fatal(err)
}
// Get final complete object
finalObject := result.Object
fmt.Printf("Final object: %v\n", finalObject)
OnError Callback
StreamObject surfaces stream-level errors (distinct from schema parse/validation errors) via the OnError callback, which is called before the function returns its final error:
result, err := ai.StreamObject(ctx, ai.StreamObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate data",
OnError: func(ctx context.Context, err error) {
log.Printf("Stream error: %v", err) // Your error logging logic here
},
})
Output Strategy
You can use both functions with different output strategies: object, array, enum, or no-schema.
Object
The default output strategy is object, which returns the generated data as an object. You don't need to specify the output strategy if you want to use the default.
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate a person profile.",
// OutputMode: "object", // default, can be omitted
})
Array
If you want to generate an array of objects, you can set the output strategy to array. When you use the array output strategy, the schema specifies the shape of an array element.
With StreamObject, you can receive the partial array through OnChunk as it is generated:
// Schema for a single hero
heroSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"class": map[string]interface{}{
"type": "string",
"description": "Character class, e.g. warrior, mage, or thief.",
},
"description": map[string]interface{}{"type": "string"},
},
"required": []string{"name", "class", "description"},
}
result, _ := ai.StreamObject(ctx, ai.StreamObjectOptions{
Model: model,
OutputMode: "array",
Schema: schema.NewSimpleJSONSchema(heroSchema),
Prompt: "Generate 3 hero descriptions for a fantasy role playing game.",
OnChunk: func(partialObject interface{}) {
fmt.Printf("Partial array so far: %v\n", partialObject)
},
})
// The final array is available once StreamObject returns
for _, hero := range result.Array {
fmt.Printf("Hero: %v\n", hero)
}
Note: To receive fully validated elements one at a time as they complete, use the non-deprecated
StreamTextwithArrayOutput[T]()andai.ElementStreamWithOutput()shown above under Element Streaming for Arrays.
Enum
If you want to generate a specific enum value, e.g. for classification tasks, you can set the output strategy to enum and provide a list of possible values in the EnumValues parameter.
Note: Enum output is only available with
GenerateObject.
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
OutputMode: "enum",
EnumValues: []string{"action", "comedy", "drama", "horror", "sci-fi"},
Prompt: "Classify the genre of this movie plot: " +
"\"A group of astronauts travel through a wormhole in search of a " +
"new habitable planet for humanity.\"",
})
if err != nil {
log.Fatal(err)
}
// result.EnumValue contains the selected enum value
fmt.Printf("Genre: %s\n", result.EnumValue) // Output: sci-fi
No Schema
In some cases, you might not want to use a schema, for example when the data is a dynamic user request. You can use the OutputMode setting to set the output format to no-schema in those cases and omit the schema parameter.
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
OutputMode: "no-schema",
Prompt: "Generate a lasagna recipe.",
})
if err != nil {
log.Fatal(err)
}
// result.Object contains the parsed value without schema validation
fmt.Printf("Object: %v\n", result.Object)
Schema Name and Description
You can optionally specify a name and description for the schema. These are used by some providers for additional LLM guidance, e.g. via tool or schema name.
recipeSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"ingredients": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"amount": map[string]interface{}{"type": "string"},
},
},
},
"steps": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
},
},
"required": []string{"name", "ingredients", "steps"},
}
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
SchemaName: "Recipe",
SchemaDescription: "A recipe for a dish.",
Schema: schema.NewSimpleJSONSchema(recipeSchema),
Prompt: "Generate a lasagna recipe.",
})
Error Handling
When GenerateObject cannot generate a valid object, it returns an error. This error occurs when the AI provider fails to generate a parsable object that conforms to the schema.
It can arise due to the following reasons:
- The model failed to generate a response
- The model generated a response that could not be parsed
- The model generated a response that could not be validated against the schema
import providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate data",
})
if err != nil {
// Check for specific error types
var noObjectErr *ai.NoObjectGeneratedError
if errors.As(err, &noObjectErr) {
fmt.Println("No object was generated:", noObjectErr.Message)
} else if providererrors.IsValidationError(err) {
fmt.Println("Object validation failed")
} else {
fmt.Printf("Error: %v\n", err)
}
return
}
Advanced Usage
Using Go Structs for Type Safety
For better type safety, define Go structs and generate schemas from them:
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address Address `json:"address"`
}
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
// Generate a JSON schema directly from the struct's json tags
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: ai.SchemaFor[Person](),
Prompt: "Generate a person profile.",
})
// result.Object is already unmarshaled; re-marshal it to decode into a typed struct
objBytes, _ := json.Marshal(result.Object)
var person Person
json.Unmarshal(objBytes, &person)
fmt.Printf("Person: %+v\n", person)
Information Extraction
Extract structured information from unstructured text:
articleSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"title": map[string]interface{}{"type": "string"},
"author": map[string]interface{}{"type": "string"},
"date": map[string]interface{}{"type": "string"},
"summary": map[string]interface{}{"type": "string"},
"keywords": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
},
},
}
article := "..." // your article text
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema.NewSimpleJSONSchema(articleSchema),
Prompt: fmt.Sprintf("Extract key information from this article: %s", article),
})
// result.Object is already unmarshaled; re-marshal it to decode into a typed value
objBytes, _ := json.Marshal(result.Object)
var extractedInfo map[string]interface{}
json.Unmarshal(objBytes, &extractedInfo)
fmt.Printf("Extracted: %+v\n", extractedInfo)
Classification
Use enum mode for classification tasks:
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
OutputMode: "enum",
EnumValues: []string{"positive", "negative", "neutral"},
Prompt: "Classify the sentiment of this review: \"This product is amazing!\"",
})
fmt.Printf("Sentiment: %s\n", result.EnumValue) // Output: positive
Synthetic Data Generation
Generate synthetic test data:
userSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"username": map[string]interface{}{"type": "string"},
"email": map[string]interface{}{"type": "string"},
"age": map[string]interface{}{"type": "integer", "minimum": 18, "maximum": 80},
"role": map[string]interface{}{"type": "string", "enum": []string{"admin", "user", "moderator"}},
},
}
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
OutputMode: "array",
Schema: schema.NewSimpleJSONSchema(userSchema),
Prompt: "Generate 5 realistic test users for a web application.",
})
// For array mode, elements are available in result.Array
for i, user := range result.Array {
fmt.Printf("User %d: %+v\n", i+1, user)
}
Callbacks
OnFinish Callback
Monitor when object generation completes:
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate data",
OnFinish: func(ctx context.Context, result *ai.GenerateObjectResult, userContext interface{}) {
fmt.Printf("Object generated\n")
fmt.Printf("Usage: %+v\n", result.Usage)
fmt.Printf("Finish reason: %s\n", result.FinishReason)
},
})
OnChunk Callback (Streaming)
Process partial objects as they arrive during streaming (note: StreamObject blocks until generation completes; OnChunk is a callback, not a channel):
result, _ := ai.StreamObject(ctx, ai.StreamObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate data",
OnChunk: func(partialObject interface{}) {
fmt.Printf("Partial: %v\n", partialObject)
},
})
Validation
The Go AI SDK automatically validates generated objects against your schema. If validation fails, an error is returned:
import providererrors "github.com/digitallysavvy/go-ai/pkg/provider/errors"
result, err := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema,
Prompt: "Generate data",
})
if err != nil {
if providererrors.IsValidationError(err) {
fmt.Println("Generated object does not match schema")
// Handle validation failure
}
}
Best Practices
- Define Clear Schemas: Provide detailed schemas with descriptions for better results
- Use Enums for Classification: Use enum mode for fixed-choice classification tasks
- Stream Long Objects: Use
StreamObjectfor large or complex objects - Handle Errors: Always check for validation and generation errors
- Use Type-Safe Structs: Define Go structs for compile-time type safety
- Add Descriptions: Include field descriptions in your schema to guide the model
- Test Schemas: Validate your schemas work correctly before production use
Common Patterns
Multi-Entity Extraction
Extract multiple entities from text:
entitiesSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"people": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"role": map[string]interface{}{"type": "string"},
},
},
},
"organizations": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"type": map[string]interface{}{"type": "string"},
},
},
},
"locations": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
},
},
}
text := "Apple CEO Tim Cook announced a new initiative in Cupertino..."
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema.NewSimpleJSONSchema(entitiesSchema),
Prompt: fmt.Sprintf("Extract all people, organizations, and locations from: %s", text),
})
Form Data Extraction
Extract form data from images or documents:
formSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{"type": "string"},
"email": map[string]interface{}{"type": "string"},
"phone": map[string]interface{}{"type": "string"},
"address": map[string]interface{}{"type": "string"},
"dob": map[string]interface{}{"type": "string"},
},
}
result, _ := ai.GenerateObject(ctx, ai.GenerateObjectOptions{
Model: model,
Schema: schema.NewSimpleJSONSchema(formSchema),
Prompt: "Extract form data from this document: ...",
})
Next Steps
- Learn about Tools and Tool Calling
- Explore Embeddings
- See Examples