Du har shippat en feature som anropar GPT-4 för att extrahera strukturerad data från användarinskickade fakturor. I utveckling fungerar det perfekt — modellen returnerar ett rent JSON-objekt varje gång. Sedan i produktion, kl 02.00, får du en Sentry-alert: JSON.parse: unexpected token. Modellen bestämde sig för att föregå sitt svar med "Sure! Here's the JSON you asked for:" innan själva payloaden. En vecka senare, samma feature, annan bugg: modellen returnerar totalAmount istället för total_amount, och din downstream-databasskrivning släpper fältet tyst. Om du har prompt-engineerat dig runt pålitlighet på LLM-utgångar, är OpenAI Structured Outputs fixen du har väntat på.

Structured Outputs, släppt av OpenAI i augusti 2024, låter dig leverera ett JSON Schema via parametern response_format och ta emot ett garanterat-giltigt svar som matchar det schemat exakt. Det skiljer sig från det äldre JSON-läget ({"type": "json_object"}), som bara säkerställde att utgången var giltig JSON — inte att den matchade någon särskild form. Det är också distinkt från function calling, som dirigerar modellens utgång till ett tool call men lägger till ett eget lager av ceremoni. Structured Outputs är den renaste vägen: beskriv formen du vill ha, få tillbaka exakt den formen, varje gång. Under huven använder OpenAI constrained decoding — modellens token-sampling styrs av ditt schema så att modellen bokstavligen inte kan producera ett ogiltigt svar.

Din första Structured Output

python
from openai import OpenAI
import json

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {
            "role": "user",
            "content": "Extract the vendor name, invoice number, and total amount from this text: "
                       "Invoice #INV-2024-0892 from Acme Supplies Ltd. Total due: $1,450.00"
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "invoice_extract",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "vendor_name":     {"type": "string"},
                    "invoice_number":  {"type": "string"},
                    "total_amount":    {"type": "number"}
                },
                "required": ["vendor_name", "invoice_number", "total_amount"],
                "additionalProperties": False
            }
        }
    }
)

data = json.loads(response.choices[0].message.content)
print(data)
# {"vendor_name": "Acme Supplies Ltd", "invoice_number": "INV-2024-0892", "total_amount": 1450.0}

Tre saker att lägga märke till. För det första är modellen gpt-4o-2024-08-06 — Structured Outputs kräver en modell som uttryckligen stödjer det (snapshotten -2024-08-06 eller senare för GPT-4o, eller gpt-4o-mini). För det andra är response_format.type "json_schema", inte "json_object". För det tredje är "strict": True det som ger dig garantin — utan det är du tillbaka i best-effort-territorium. Fältet name är en etikett modellen ser; det påverkar inte parsningen men gör dina API-loggar läsbara.

Designa JSON-schemat

Här är ett mer realistiskt schema för en produktkatalogs-extraktionsuppgift — den sort du skulle använda för att hämta strukturerad data från ostrukturerade produktbeskrivningar, e-handelslistningar eller PDF-datablad. Använd JSON Schema Generator för att bygga och validera ditt schema visuellt innan du kopplar in det i dina API-anrop.

json
{
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "The full commercial name of the product"
    },
    "sku": {
      "type": "string",
      "description": "Stock keeping unit identifier"
    },
    "price_usd": {
      "type": "number",
      "description": "Price in US dollars, numeric only"
    },
    "in_stock": {
      "type": "boolean"
    },
    "categories": {
      "type": "array",
      "items": { "type": "string" }
    },
    "dimensions": {
      "type": "object",
      "properties": {
        "width_cm":  { "type": "number" },
        "height_cm": { "type": "number" },
        "depth_cm":  { "type": "number" }
      },
      "required": ["width_cm", "height_cm", "depth_cm"],
      "additionalProperties": false
    }
  },
  "required": [
    "product_name", "sku", "price_usd",
    "in_stock", "categories", "dimensions"
  ],
  "additionalProperties": false
}
  • Alla properties måste finnas i required i strict mode. Du kan inte ha valfria fält. Om ett fält kanske inte finns i källdatan, använd en typ-union: {"type": ["string", "null"]} och inkludera det alltid i required.
  • additionalProperties måste vara false på varje objektnivå. Det gäller rekursivt — dina nästlade objekt behöver det också, inte bara roten.
  • Stödda typer i strict mode: string, number, integer, boolean, null, array, object. Typ-unioner (["string", "null"]) är tillåtna.
  • Inga $ref eller rekursiva scheman i strict mode. Allt måste vara inlinat. Om du behöver en återanvändbar definition, kopiera den.
  • Lägg till description-fält generöst. Modellen läser dem. Att säga "Price in US dollars, numeric only — do not include currency symbols" ger dig renare utgång än att hoppas att modellen gissar rätt.
  • Enums fungerar. {"type": "string", "enum": ["pending", "shipped", "delivered"]} stöds fullt ut och modellen kommer bara någonsin emittera ett av dessa tre värden.

Strict mode vs non-strict

python
# Strict mode — guaranteed conformance, tighter schema rules
response_format_strict = {
    "type": "json_schema",
    "json_schema": {
        "name": "product_extract",
        "strict": True,   # <-- the key flag
        "schema": product_schema
    }
}

# Non-strict — more schema flexibility, best-effort conformance
response_format_lenient = {
    "type": "json_schema",
    "json_schema": {
        "name": "product_extract",
        "strict": False,
        "schema": product_schema
    }
}

Med strict: True pre-processar OpenAI ditt schema första gången det används och cachar den constrained decodern. Första anropet med ett nytt schema tar lite längre tid; efterföljande anrop med samma schema är snabba. Vad du får i utbyte: modellutgången är strukturellt garanterad — du kan anropa json.loads() och sedan komma åt fält direkt utan defensiva kontroller. Vad du ger upp: $ref, anyOf mellan strukturella varianter, och rekursiva scheman stöds inte. Non-strict mode accepterar ett bredare urval av JSON Schema- funktioner men faller tillbaka till best-effort — modellen försöker följa schemat men är inte begränsad på tokennivå. För produktions-extraktionspipelines, använd alltid strict mode. Schema-restriktionerna är hanterbara när du förstår dem.

Nästlade objekt och arrayer

Nästlade strukturer fungerar bra, men varje nästlat objekt behöver sitt eget "additionalProperties": false och sin egen "required"-array som listar alla properties. Ett vanligt misstag är att tillämpa strict-regler på rot-objektet och glömma barnen — OpenAI avvisar schemat med ett valideringsfel.

python
from openai import OpenAI
import json

client = OpenAI()

order_schema = {
    "type": "object",
    "properties": {
        "order_id": {"type": "string"},
        "customer": {
            "type": "object",
            "properties": {
                "name":  {"type": "string"},
                "email": {"type": "string"}
            },
            "required": ["name", "email"],
            "additionalProperties": False   # required on nested objects too
        },
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "quantity":    {"type": "integer"},
                    "unit_price":  {"type": "number"}
                },
                "required": ["description", "quantity", "unit_price"],
                "additionalProperties": False  # required on array item schemas too
            }
        },
        "total_usd": {"type": "number"}
    },
    "required": ["order_id", "customer", "line_items", "total_usd"],
    "additionalProperties": False
}

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{
        "role": "user",
        "content": (
            "Parse this order: Order #ORD-5531 for Jane Smith ([email protected]). "
            "2x Wireless Keyboard at $49.99 each, 1x USB Hub at $29.99. Total: $129.97"
        )
    }],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "order_extract",
            "strict": True,
            "schema": order_schema
        }
    }
)

order = json.loads(response.choices[0].message.content)
for item in order["line_items"]:
    print(f"${item['unit_price']:.2f} x{item['quantity']}  {item['description']}")

Hantera refusals

Även med Structured Outputs kan modellen vägra att svara — typiskt när prompten triggar en content policy (att be den extrahera data från något skadligt). När det händer är finish_reason "stop" men message.content är null och message.refusal innehåller vägrans-texten. Om du inte kollar för detta får du ett AttributeError när du försöker anropa json.loads(None). Se också upp för finish_reason == "length" — om svaret kapades på grund av max_tokens blir JSON:en ofullständig och oparsebar oavsett Structured Outputs.

python
import json
from openai import OpenAI

client = OpenAI()

def extract_invoice(raw_text: str) -> dict | None:
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": f"Extract invoice fields: {raw_text}"}],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "invoice_extract",
                "strict": True,
                "schema": invoice_schema
            }
        },
        max_tokens=1024
    )

    choice = response.choices[0]

    if choice.finish_reason == "length":
        raise ValueError("Response truncated — increase max_tokens or simplify your schema")

    if choice.message.refusal:
        # Model refused to answer — log and return None rather than crashing
        print(f"Model refused: {choice.message.refusal}")
        return None

    return json.loads(choice.message.content)


result = extract_invoice("Invoice #2024-441 from BuildRight Inc., due $3,200 by Dec 15")
if result:
    print(result["vendor_name"], result["total_amount"])

Använd samma schema för validering

Ett underanvänt mönster: använd samma JSON Schema som du skickar till OpenAI för att också validera data som kommer in från andra källor — webhooks, filuppladdningar, tredjeparts-API:er. Det ger dig en enda källa till sanning för din dataform. I Python, använd jsonschema-biblioteket. I Node.js, använd Ajv. Du kan också klistra in ditt schema i JSON Validator för att göra en snabb manuell sanity-check utan att skriva någon kod.

python
import json
import jsonschema
from jsonschema import validate, ValidationError

# The same schema used in your OpenAI call
invoice_schema = {
    "type": "object",
    "properties": {
        "vendor_name":    {"type": "string"},
        "invoice_number": {"type": "string"},
        "total_amount":   {"type": "number"}
    },
    "required": ["vendor_name", "invoice_number", "total_amount"],
    "additionalProperties": False
}

def validate_invoice(data: dict) -> bool:
    try:
        validate(instance=data, schema=invoice_schema)
        return True
    except ValidationError as e:
        print(f"Validation failed: {e.message}")
        print(f"  Path: {' -> '.join(str(p) for p in e.path)}")
        return False


# Validate a payload from a webhook — same schema, zero extra work
webhook_payload = json.loads(request_body)
if not validate_invoice(webhook_payload):
    return HTTPResponse(status=400, body="Invalid invoice payload")

# Validate the OpenAI output too, for belt-and-suspenders safety
llm_output = json.loads(openai_response.choices[0].message.content)
assert validate_invoice(llm_output), "LLM output failed schema validation — check schema definition"
print(f"Processing invoice {llm_output['invoice_number']} for ${llm_output['total_amount']}")
Bygg ditt schema snabbare: Använd JSON Schema Generator för att skapa och förfina ditt schema visuellt — klistra in ett JSON-exempelobjekt och det genererar ett startschema automatiskt. Kopiera resultatet direkt in i ditt OpenAI response_format.

JavaScript / Node.js-version

OpenAI Node.js-SDK:et speglar Python-API:t nästan exakt. Huvudskillnaden är att strict sitter inuti json_schema-objektet på samma sätt, och du parsar svaret med JSON.parse(). Validering med Ajv är Node.js- motsvarigheten till Pythons jsonschema-bibliotek — det är snabbare och har utmärkt TypeScript-stöd.

js
import OpenAI from "openai";
import Ajv from "ajv";

const client = new OpenAI();
const ajv = new Ajv();

const invoiceSchema = {
  type: "object",
  properties: {
    vendor_name:    { type: "string" },
    invoice_number: { type: "string" },
    total_amount:   { type: "number" },
    line_items: {
      type: "array",
      items: {
        type: "object",
        properties: {
          description: { type: "string" },
          amount:      { type: "number" }
        },
        required: ["description", "amount"],
        additionalProperties: false
      }
    }
  },
  required: ["vendor_name", "invoice_number", "total_amount", "line_items"],
  additionalProperties: false
};

const validateInvoice = ajv.compile(invoiceSchema);

async function extractInvoice(rawText) {
  const response = await client.chat.completions.create({
    model: "gpt-4o-2024-08-06",
    messages: [{ role: "user", content: `Extract invoice fields: ${rawText}` }],
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "invoice_extract",
        strict: true,
        schema: invoiceSchema
      }
    }
  });

  const choice = response.choices[0];

  if (choice.message.refusal) {
    throw new Error(`Model refused: ${choice.message.refusal}`);
  }

  const data = JSON.parse(choice.message.content);

  // Validate even though strict mode guarantees structure —
  // useful for catching schema drift between environments
  if (!validateInvoice(data)) {
    console.error("Schema validation errors:", validateInvoice.errors);
    throw new Error("Output failed schema validation");
  }

  return data;
}

const invoice = await extractInvoice(
  "Invoice #INV-881 from Nordic Parts AS. " +
  "3x Brake Pads at $28.50 each. Total: $85.50"
);

console.log(`${invoice.vendor_name} — Invoice ${invoice.invoice_number}`);
invoice.line_items.forEach(item =>
  console.log(`  ${item.description}: $${item.amount}`)
);

Avslutningsvis

Structured Outputs eliminerar en hel kategori produktionsbuggar — den sort där modellen returnerar nästan-rätt JSON som bryter din parser kl 02.00. Workflowet är okomplicerat: designa ditt schema omsorgsfullt (varje nästlat objekt behöver additionalProperties: false och en full required-array), sätt strict: true, och hantera refusals och truncation uttryckligen. När schemat är på plats kan du återanvända det över hela din stack — i OpenAI-anropet, i webhook-validering, i dina testfixtures — med bibliotek som jsonschema (Python) eller Ajv (Node.js). Om du börjar från scratch med ett schema är JSON Schema Generator det snabbaste sättet att få ett fungerande basschema från en exempel-payload. Dagarna då du promptingenjör:ade dig till pålitlig JSON-utgång är förbi — använd verktyget som byggdes för det.