Skip to content

Structured output

By default an interaction answers in prose. Give it a JSON Schema and it answers with JSON that matches the schema instead, so your code can read fields rather than parse sentences. See it in tutorial 3, Typed answers, which turns a messy recipe into JSON.

Put the schema in json/, and point the interaction’s schema.solution at it:

json/recipe.json
{
"type": "object",
"properties": {
"title": { "type": "string" },
"servings": { "type": ["integer", "null"] },
"ingredients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"item": { "type": "string" },
"quantity": { "type": ["string", "null"] }
},
"required": ["item", "quantity"],
"additionalProperties": false
}
},
"steps": { "type": "array", "items": { "type": "string" } }
},
"required": ["title", "servings", "ingredients", "steps"],
"additionalProperties": false
}
interactions/one-shot/extract-recipe.mtr
agent = {
name = "Extractor"
agent = @ref("extractor")
}
task = @prompt("extract-recipe-task")
inputs.prompts = [
{
name = "text"
description = "Text that contains a recipe"
required = true
}
]
// The answer must match this JSON Schema, so your code can use it directly.
schema.solution = @json("recipe")

The run’s output is then a JSON string that matches the schema:

{
"title": "Grandma's pancakes",
"servings": 4,
"ingredients": [
{ "item": "flour", "quantity": "200g" },
{ "item": "eggs", "quantity": "2" },
{ "item": "milk", "quantity": "300ml" },
{ "item": "butter", "quantity": "a little" }
],
"steps": ["Whisk the flour, eggs and milk.", "Rest 10 minutes, then fry thin pancakes in butter."]
}
  • With models that support it, the schema is sent to the provider, which constrains the model’s answer to it.
  • Every answer is checked against the schema. A small fix-up is tried first (a stray code fence, say). If the answer still doesn’t match, the model is told what is wrong and tries again. After the third answer that doesn’t match, the run ends with Error, and its failureDetails say “Unable to parse JSON result”.

So a Finished run with a schema always has an output your code can read.

  • Set "additionalProperties": false on every object, and list every property under required.
  • For something that may be missing, allow null rather than leaving it out of required: "servings": { "type": ["integer", "null"] }. Without a way to say “not given”, the model has to make a value up to match the schema, as tutorial 3 shows.
  • Describe fields with "description" when the name alone isn’t obvious. The model reads it.
  • Keep it small. Ask for what your code needs, not everything that could be extracted.

You can pass responseJsonSchema (a JSON Schema, as a string) when you start a run, which overrides the interaction’s schema for that run only. A schema that isn’t valid JSON Schema is refused before the run starts, with api:interactions:response-schema-invalid.

const run = await mt.interactions.run("extract-recipe", { inputs: { text } });
const recipe = JSON.parse(run.output!) as Recipe;

Tutorial 3, Typed answers builds this end to end, with typed classes in each language. Its C# reads the JSON with a source-generated serializer, which a .NET file-based app needs: those are set up for ahead-of-time compilation, which turns reflection-based JSON off, so the line above throws there.