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
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.
{
"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
requiredi 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 irequired. additionalPropertiesmåste varafalsepå 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
$refeller 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
# 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.
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.
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.
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']}")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.
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.