Disenar extraccion estructurada Claude como contrato de datos: JSON Schema defensivo (required reales, union nullable, enum con valvula de escape), tool_choice forzado y parseo desde tool_use.input.
Diseñar la extracción estructurada de un modelo Claude como un contrato de datos verificable, no como prosa que luego se parsea. Se define un JSON Schema defensivo y se fuerza tool_choice para que el modelo emita exactamente ese schema: required que reflejan campos realmente presentes en la fuente, uniones nullable para opcionales (en vez de defaults silenciosos como '' o 0), y enums con válvula de escape ('other' + *_details) para no perder señal cuando el dato no encaja en el catálogo. El resultado es una salida parseable de forma determinista, auditable y resistente a alucinación de campos. [DOC]
json.loads(text) se rompe de forma intermitente (code fences, prosa adicional). [INFERENCIA]'', 0, "N/A") que contamina el dataset aguas abajo. [DOC]tool_choice="auto". Forzar mata la decisión real. [INFERENCIA] → ver kata multi_tool_choice_boundary.null. [DOC]input_schema defensivo; (2) configuración de tool_choice; (3) ruta de parseo desde tool_use.input; (4) gate de validación contra schema con salida a retry/escalada. [DOC]required) de lo que aparece a veces (→ opcional). No marques required por deseo: marca por presencia real. [DOC]["string", "null"], nunca string con default ''. Ausente debe ser representable como null, no como cadena vacía. [DOC]'other' y un campo hermano *_details para capturar el valor textual cuando no encaja. El catálogo evoluciona con evidencia en vez de perder filas. [DOC]type=object + additionalProperties=false: el modelo no puede inventar campos fuera del contrato. [CONFIG]input_schema. El schema vive en la definición de la herramienta, no en el prompt en prosa. [DOC]tool_choice solo cuando no hay decisión de tool que tomar. Si la única acción válida es emitir la estructura → tool_choice={"type":"tool","name":"..."}. Si el modelo debe elegir entre varias → auto, no fuerces. [DOC]tool_use.input, nunca desde el texto. El consumidor lee el bloque tool_use tipado; ni regex ni json.loads sobre prosa. [DOC]stop_reason anómalo, refusal, schema inválido). [DOC]# GOOD: defensive schema, forced tool_choice, parse from tool_use.input
extract_invoice = {
"name": "extract_invoice",
"description": "Emit invoice fields exactly as they appear in the source document.",
"input_schema": {
"type": "object",
"additionalProperties": False, # closed: no invented fields
"properties": {
"invoice_id": {"type": "string"}, # required: always present
"total_amount": {"type": "number"}, # required: always present
"due_date": {"type": ["string", "null"]}, # optional -> nullable, not ""
"status": {
"type": "string",
"enum": ["paid", "pending", "overdue", "other"], # escape valve
},
"status_details": {"type": ["string", "null"]}, # captures 'other'
},
"required": ["invoice_id", "total_amount", "status"],
},
}
resp = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
tools=[extract_invoice],
tool_choice={"type": "tool", "name": "extract_invoice"}, # forced
messages=[{"role": "user", "content": document_text}],
)
block = next(b for b in resp.content if b.type == "tool_use")
data = block.input # typed dict, no json.loads on prose
validate(data, extract_invoice["input_schema"]) # gate before accepting# ANTI: "return JSON" in prose + json.loads(text); empty-string defaults; closed enum
resp = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{
"role": "user",
"content": document_text + "\n\nReturn the invoice as JSON.",
}],
)
text = resp.content[0].text
data = json.loads(text) # breaks when the model adds prose or a code fence
due = data.get("due_date", "") # "" hides a genuinely-absent value
status = data["status"] # closed enum drops every unforeseen real case
# ...and on failure: regex fallback over free text -> silent garbage downstreamstop_reason="max_tokens" con tool_use truncado → el JSON está incompleto; no lo aceptes, sube max_tokens o particiona la fuente, y reintenta. [INFERENCIA]text y no tool_use pese a forzar la tool → trátalo como refusal/error; ruta a escalada, nunca parsees el texto. [INFERENCIA]required llega ausente en datos reales → el inventario estaba mal; degrádalo a nullable, no fuerces un default. [INFERENCIA]'other' → señal de catálogo incompleto; revisa *_details y promueve los casos recurrentes a categorías nuevas. [INFERENCIA]null (columna NOT NULL) → resuélvelo en el consumidor (default explícito documentado), no metiendo '' en el schema. [SUPUESTO]required corresponde a algo realmente presente en la fuente (no a un deseo)? [DOC]nullable y se eliminó todo default ''/0/"N/A"? Ausente = null/unclear, no cadena vacía. [DOC]additionalProperties=false) con propiedades declaradas? [CONFIG]'other' + *_details)? [DOC]tool_choice se fuerza solo cuando no hay una decisión de tool legítima que tomar? [DOC]tool_use.input y nunca desde texto en prosa ni con regex? [DOC]assets/structured-output-design-contract.json y pasa scripts/check.sh con fixtures determinísticas (positivas y negativas)? [CÓDIGO]assets/structured-output-design-contract.json define el paquete JSON que documenta una tool estructurada. [CÓDIGO]assets/json-schema-policy.json exige type=object, additionalProperties=false, required reales y propiedades declaradas. [CÓDIGO]assets/nullable-policy.json prohíbe defaults falsos y requiere unión con null. [CÓDIGO]assets/enum-escape-policy.json exige other + campo *_details para todo enum cerrado. [CÓDIGO]assets/tool-choice-policy.json fija tool_choice={"type":"tool","name":...} cuando la única acción válida es emitir la estructura. [CÓDIGO]assets/refusal-error-policy.json exige canal de error/refusal y parseo desde tool_use.input. [CÓDIGO]scripts/validate_structured_output_design.py valida el paquete offline; scripts/check.sh ejecuta fixtures positivas y negativas. [CÓDIGO]05. [CONFIG]katas-defensive-structured-extraction, katas-headless-code-review. [CONFIG]validation-retry-design, self-correction-loops, provenance-engineering. [CONFIG]JaviMontano/jm-adk-beta
fdad39c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.