Du har shippet en feature som kaller GPT-4 for å trekke ut strukturerte data fra brukerinnsendte fakturaer.
I utvikling fungerer det perfekt — modellen returnerer et rent JSON-objekt hver gang. Så i produksjon,
kl. 02.00, får du en Sentry-alert: JSON.parse: unexpected token. Modellen bestemte seg for å innlede svaret
med "Sure! Here's the JSON you asked for:" før selve payloaden. En uke senere, samme feature,
annen bug: modellen returnerer totalAmount i stedet for total_amount, og din
downstream-databaseskriving slipper feltet i stillhet. Hvis du har prompt-engineert deg rundt pålitelighet i LLM-output,
er OpenAI Structured Outputs
fiksen du har ventet på.
Structured Outputs, lansert av OpenAI i august 2024, lar deg levere et
JSON Schema
via response_format-parameteren og motta et garantert gyldig svar som
matcher det skjemaet nøyaktig. Dette er annerledes enn den eldre JSON-modusen ({"type": "json_object"}),
som bare sikret at outputtet var gyldig JSON — ikke at det matchet en spesiell form. Det er også adskilt
fra function calling, som ruter modellens output inn i et tool call, men legger til sitt eget seremoni-lag.
Structured Outputs er den reneste veien: beskriv formen du vil ha, få nøyaktig den formen tilbake, hver gang.
Under panseret bruker OpenAI constrained decoding — modellens token-sampling styres av skjemaet ditt, så
modellen bokstavelig talt ikke kan produsere et ugyldig svar.
Ditt 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 å legge merke til. For det første er modellen gpt-4o-2024-08-06 — Structured Outputs
krever en modell som eksplisitt støtter det (snapshot -2024-08-06 eller nyere for GPT-4o, eller
gpt-4o-mini). For det andre er response_format.type "json_schema", ikke
"json_object". For det tredje er "strict": True det som gir deg garantien —
uten det er du tilbake i best-effort-territorium. Feltet name er en etikett modellen ser;
det påvirker ikke parsing, men gjør API-loggene dine lesbare.
Designe JSON-skjemaet
Her er et mer realistisk skjema for en produktkatalog-ekstraksjonsoppgave — den typen du ville brukt til å trekke ut strukturerte data fra ustrukturerte produktbeskrivelser, e-handelsoppføringer eller PDF-datablad. Bruk JSON Schema Generator for å bygge og validere skjemaet visuelt før du kobler det inn i API-kallene dine.
{
"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 må være i
requiredi strict mode. Du kan ikke ha valgfrie felt. Hvis et felt kanskje ikke finnes i kildedataene, bruk en type-union:{"type": ["string", "null"]}og inkluder det alltid irequired. additionalPropertiesmå værefalsepå hvert objektnivå. Dette gjelder rekursivt — de nestede objektene dine trenger det også, ikke bare roten.- Støttede typer i strict mode:
string,number,integer,boolean,null,array,object. Type-unioner (["string", "null"]) er tillatt. - Ingen
$refeller rekursive skjemaer i strict mode. Alt må være inlinet. Trenger du en gjenbrukbar definisjon, kopier den. - Legg til
description-felt rundhåndet. Modellen leser dem. Å si "Price in US dollars, numeric only — do not include currency symbols" gir deg renere output enn å håpe at modellen gjetter riktig. - Enums fungerer.
{"type": "string", "enum": ["pending", "shipped", "delivered"]}er fullt støttet, og modellen vil kun noen gang sende ut én av disse tre verdiene.
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 preprocesserer OpenAI skjemaet ditt første gang det brukes,
og cacher den constrained decoderen. Det første kallet med et nytt skjema tar litt lengre tid; etterfølgende
kall med samme skjema er raske. Hva du får i retur: modellens output er strukturelt
garantert — du kan kalle json.loads() og deretter få tilgang til felt direkte uten
defensive sjekker. Hva du gir opp: $ref, anyOf på tvers av strukturelle varianter,
og rekursive skjemaer er ikke støttet. Non-strict mode aksepterer et bredere utvalg av
JSON Schema-
funksjoner, men faller tilbake til best-effort — modellen prøver å følge skjemaet, men er ikke begrenset
på tokennivå. For produksjons-ekstraksjonspipelines, bruk alltid strict mode. Skjema-restriksjonene
er håndterbare når du først forstår dem.
Nestede objekter og arrays
Nestede strukturer fungerer godt, men hvert nestede objekt trenger sin egen "additionalProperties": false
og sin egen "required"-array som lister alle properties. En vanlig feil er å bruke strict-regler
på rotobjektet og glemme barna — OpenAI avviser skjemaet med en valideringsfeil.
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 av refusals
Selv med Structured Outputs kan modellen nekte å svare — typisk når prompten
trigger en content policy (å be den trekke ut data fra noe skadelig). Når det skjer, er
finish_reason "stop", men message.content er null,
og message.refusal inneholder avvisningsteksten. Hvis du ikke sjekker for dette, får du
en AttributeError når du prøver å kalle json.loads(None). Pass også på
finish_reason == "length" — hvis svaret ble kuttet på grunn av max_tokens,
blir JSON-en ufullstendig og uparserbar uansett 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"])Bruke samme skjema til validering
Et underutnyttet mønster: bruk samme JSON Schema
som du sender til OpenAI, til også å validere data som kommer inn fra andre kilder — webhooks, filopplastninger,
tredjeparts-API-er. Det gir deg én kilde til sannhet for dataformen din. I Python, bruk
jsonschema-biblioteket. I Node.js, bruk Ajv.
Du kan også lime skjemaet inn i JSON Validator for å gjøre en rask
manuell sanity-sjekk uten å 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-en din.JavaScript / Node.js-versjon
OpenAI Node.js-SDK-et
speiler Python-API-et nesten nøyaktig. Hovedforskjellen er at strict sitter inne i
json_schema-objektet på samme måte, og du parser svaret med JSON.parse().
Validering med Ajv er Node.js-
motstykket til Pythons jsonschema-bibliotek — det er raskere og har utmerket TypeScript-støtte.
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}`)
);Oppsummering
Structured Outputs eliminerer en hel kategori produksjonsbugs — den typen der modellen
returnerer nesten-riktig JSON som ødelegger parseren din kl. 02.00. Arbeidsflyten er grei: design
skjemaet ditt nøye (hvert nestet objekt trenger additionalProperties: false og en full
required-array), sett strict: true, og håndter refusals og truncation eksplisitt.
Når skjemaet er på plass, kan du gjenbruke det på tvers av stacken — i OpenAI-kallet, i webhook-validering,
i testfixturene dine — med biblioteker som jsonschema (Python) eller
Ajv (Node.js). Hvis du starter fra
bunnen med et skjema, er JSON Schema Generator den raskeste måten
å få et fungerende grunnskjema fra en eksempel-payload. Dagene med å prompt-engineere seg til pålitelig
JSON-output er forbi — bruk verktøyet som ble bygget for det.