# 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()`](https://goaisdk.com/docs/reference/ai/generate-text.md) and [`ai.StreamText()`](https://goaisdk.com/docs/reference/ai/stream-text.md). 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()` and `ai.StreamObject()` functions are **deprecated**. Use the `Output` option 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

```go
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

```go
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

```go
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)

```go
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.

```go
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:

```go
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:

```go
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` / `StreamText` with `Output` instead.

## 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.

```go
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:

```go
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()`](https://goaisdk.com/docs/reference/ai/stream-object.md) 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.

```go
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:

```go
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.

```go
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:

```go
// 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 `StreamText` with `ArrayOutput[T]()` and `ai.ElementStreamWithOutput()` shown above under [Element Streaming for Arrays](#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`.

```go
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.

```go
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.

```go
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

```go
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:

```go
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:

```go
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:

```go
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:

```go
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:

```go
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):

```go
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:

```go
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

1. **Define Clear Schemas**: Provide detailed schemas with descriptions for better results
2. **Use Enums for Classification**: Use enum mode for fixed-choice classification tasks
3. **Stream Long Objects**: Use `StreamObject` for large or complex objects
4. **Handle Errors**: Always check for validation and generation errors
5. **Use Type-Safe Structs**: Define Go structs for compile-time type safety
6. **Add Descriptions**: Include field descriptions in your schema to guide the model
7. **Test Schemas**: Validate your schemas work correctly before production use

## Common Patterns

### Multi-Entity Extraction

Extract multiple entities from text:

```go
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:

```go
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](https://goaisdk.com/docs/ai-sdk-core/tools-and-tool-calling.md)
- Explore [Embeddings](https://goaisdk.com/docs/ai-sdk-core/embeddings.md)
- See [Examples](https://github.com/digitallysavvy/go-ai/tree/main/examples)

## See Also

- [Generating Text](https://goaisdk.com/docs/ai-sdk-core/generating-text.md)
- [Settings](https://goaisdk.com/docs/ai-sdk-core/settings.md)
- [Error Handling](https://goaisdk.com/docs/ai-sdk-core/error-handling.md)
- [API Reference: GenerateObject](https://goaisdk.com/docs/reference/ai/generate-object.md)
- [API Reference: StreamObject](https://goaisdk.com/docs/reference/ai/stream-object.md)
