Du har shippet en feature, der kalder GPT-4 for at udtrække struktureret data fra brugerindsendte fakturaer.
I udvikling fungerer det perfekt — modellen returnerer et rent JSON-objekt hver gang. Så i produktion,
kl. 02.00, får du en Sentry-alert: JSON.parse: unexpected token. Modellen besluttede at indlede sit
svar med "Sure! Here's the JSON you asked for:" før selve payloaden. En uge senere, samme feature,
anden bug: modellen returnerer totalAmount i stedet for total_amount, og din
downstream-databaseskrivning dropper feltet stille. Hvis du har prompt-engineeret dig uden om pålidelighed i LLM-output,
er OpenAI Structured Outputs
den fix, du har ventet på.
Structured Outputs, udgivet af OpenAI i august 2024, lader dig levere et
JSON Schema
via response_format-parameteren og modtage et garanteret gyldigt svar, der
matcher det pågældende skema nøjagtigt. Det er forskelligt fra den ældre JSON-tilstand ({"type": "json_object"}),
som kun sikrede, at outputtet var gyldig JSON — ikke at det matchede en bestemt form. Det er også adskilt
fra function calling, som dirigerer modellens output ind i et tool call, men tilføjer sit eget ceremoni-lag.
Structured Outputs er den reneste vej: beskriv den form du vil have, få nøjagtig den form tilbage, hver gang.
Under motorhjelmen bruger OpenAI constrained decoding — modellens token-sampling styres af dit skema, så
modellen bogstaveligt ikke kan producere et ugyldigt svar.
Dit første 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 ting at bemærke. For det første er modellen gpt-4o-2024-08-06 — Structured Outputs
kræver en model, der eksplicit understøtter det (snapshottet -2024-08-06 eller senere for GPT-4o, eller
gpt-4o-mini). For det andet er response_format.type "json_schema", ikke
"json_object". For det tredje er "strict": True det, der giver dig garantien —
uden det er du tilbage i best-effort-territorium. Feltet name er en etiket, modellen ser;
det påvirker ikke parsning, men gør dine API-logs læselige.
Design JSON-skemaet
Her er et mere realistisk skema til en produktkatalogs-udtræksopgave — den slags du ville bruge til at trække struktureret data ud af ustrukturerede produktbeskrivelser, e-handelslistninger eller PDF-datablade. Brug JSON Schema Generator til at bygge og validere dit skema visuelt, før du kobler det ind i dine API-kald.
{
"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
}- Alle properties skal være i
requiredi strict mode. Du kan ikke have valgfrie felter. Hvis et felt måske ikke findes i kildedata, brug en type-union:{"type": ["string", "null"]}og inkludér det altid irequired. additionalPropertiesskal værefalsepå hvert objektniveau. Dette gælder rekursivt — dine nestede objekter skal også have det, ikke kun roden.- Understøttede typer i strict mode:
string,number,integer,boolean,null,array,object. Type-unions (["string", "null"]) er tilladt. - Ingen
$refeller rekursive skemaer i strict mode. Alt skal være inlinet. Har du brug for en genbrugelig definition, så kopier den. - Tilføj
description-felter rundhåndet. Modellen læser dem. At sige "Price in US dollars, numeric only — do not include currency symbols" giver dig renere output end at håbe på, at modellen gætter rigtigt. - Enums virker.
{"type": "string", "enum": ["pending", "shipped", "delivered"]}understøttes fuldt ud, og modellen vil kun nogensinde udsende én af de tre værdier.
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-processer OpenAI dit skema første gang det bruges
og cacher den constrained decoder. Det første kald med et nyt skema tager lidt længere; efterfølgende
kald med samme skema er hurtige. Hvad du får til gengæld: modelloutputtet er strukturelt
garanteret — du kan kalde json.loads() og derefter tilgå felter direkte uden
defensive tjek. Hvad du giver op: $ref, anyOf på tværs af strukturelle varianter,
og rekursive skemaer understøttes ikke. Non-strict mode accepterer et bredere udvalg af
JSON Schema-
funktioner, men falder tilbage til best-effort — modellen prøver at følge skemaet, men er ikke begrænset
på tokenniveau. Til produktions-udtræks-pipelines, brug altid strict mode. Skema-begrænsningerne
er håndterbare, når du først forstår dem.
Nestede objekter og arrays
Nestede strukturer fungerer godt, men hvert nestet objekt har brug for sin egen "additionalProperties": false
og sin egen "required"-array, der lister alle properties. En almindelig fejl er at anvende strict regler
på rod-objektet og glemme børnene — OpenAI afviser skemaet med en valideringsfejl.
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']}")Håndtering af refusals
Selv med Structured Outputs kan modellen nægte at svare — typisk når prompten
udløser en content policy (at bede den udtrække data fra noget skadeligt). Når det sker, er
finish_reason "stop", men message.content er null,
og message.refusal indeholder afvisningsteksten. Hvis du ikke tjekker for dette, får du
en AttributeError, når du prøver at kalde json.loads(None). Pas også på
finish_reason == "length" — hvis svaret blev klippet af pga. max_tokens,
vil JSON'en være ufuldstændig og uparselig uanset 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"])Brug samme skema til validering
Et underudnyttet mønster: brug det samme JSON Schema,
som du sender til OpenAI, til også at validere data, der kommer ind fra andre kilder — webhooks, filuploads,
tredjeparts-API'er. Det giver dig en enkelt kilde til sandhed for din dataform. I Python, brug
jsonschema-biblioteket. I Node.js, brug Ajv.
Du kan også indsætte dit skema i JSON Validator for at lave et hurtigt
manuelt sanity-check uden at skrive kode.
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
spejler Python-API'et næsten nøjagtigt. Hovedforskellen er, at strict sidder inde i
json_schema-objektet på samme måde, og du parser svaret med JSON.parse().
Validering med Ajv er Node.js-
modstykket til Pythons jsonschema-bibliotek — det er hurtigere og har fremragende TypeScript-understøttelse.
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}`)
);Afrunding
Structured Outputs eliminerer en hel kategori af produktionsfejl — den slags hvor modellen
returnerer næsten-rigtig JSON, der bryder din parser kl. 02.00. Workflowet er ligetil: design dit
skema omhyggeligt (hvert nestet objekt har brug for additionalProperties: false og en fuld
required-array), sæt strict: true, og håndter refusals og truncation eksplicit.
Når skemaet er på plads, kan du genbruge det på tværs af din stack — i OpenAI-kaldet, i webhook-validering,
i dine testfixtures — med biblioteker som jsonschema (Python) eller
Ajv (Node.js). Hvis du starter fra
bunden på et skema, er JSON Schema Generator den hurtigste måde
at få et fungerende basisskema fra en eksempel-payload. Dagene med at prompt-engineere sig til pålideligt
JSON-output er forbi — brug værktøjet der blev bygget til det.