Structured Output: Typed Agent Results
How to make the agent return a typed Go struct instead of free-form prose, via a JSON schema derived from your type.
What this lesson demonstrates
Everything so far returned text. Real applications want data: a PersonInfo, not a paragraph about a person. This lesson asks the model to fill in a Go struct and shows the two ways the SDK exposes structured output:
agent.WithStructuredOutput(&v)— a per-run option. Hand it a pointer; when the run finishes the SDK has decoded the model’s JSON into your value.agent.WithResponseFormat(jsonformat.MustFor[T]())— wired once intoConfig.RunOptions, so every run is constrained toT’s JSON schema. The model streams raw JSON you unmarshal yourself.
The core code
The struct is the schema — json tags become the field names the model must return. A generic helper decodes it per run:
func runFor[T any](ctx context.Context, a *agent.Agent, message string, opts ...agent.Option) (T, error) {
var v T
opts = append(opts, agent.WithStructuredOutput(&v), agent.Stream(false))
for _, err := range a.RunText(ctx, message, opts...) {
if err != nil {
return v, err
}
}
return v, nil // decoded value is ready once the run is drained
}
What to notice
- Two altitudes for the same idea.
WithStructuredOutput(&v)is convenient at a single call site and decodes for you.WithResponseFormat(...)inConfig.RunOptionsbakes the constraint into the agent so every run returns JSON — but then you own the decoding, which is whymaincollects the streamed bytes and callsjson.Unmarshal. RunOptionsare prepended to every run. Anything you put there applies without repeating it at the call site — that is how the second agent gets its response format for free while still passingStream(true)per call. The gotcha: you must still drain (range) the run before reading the decoded value.- Generics keep it reusable.
runFor[T]works for any output type — swapPersonInfofor a slice-bearing struct and nothing about the helper changes.
How it maps to Azure AI Foundry
jsonformat.MustFor[PersonInfo]() reflects over the struct’s json tags to build a JSON schema, which the SDK sends to Foundry as a response_format = json_schema constraint on the /responses call. The model is told to return exactly that shape — no prompt-engineering the format. The offline test TestPersonInfoSchema exercises jsonformat.MustFor[PersonInfo]() directly (asserting Kind == "json", Name == "PersonInfo", and a derived schema) — the exact call the response-format agent uses at runtime.
Run it
go run ./tutorial/02-agents/agents/step05_structured_output
The same person prints twice — once decoded via WithStructuredOutput, once from streamed JSON you unmarshal. The program needs Foundry; the schema and decoding helpers are tested offline, and the live call is gated behind AF_LIVE=1.
Next: step06 · Persisted Conversation