Structured outputs: JSON confiable para agentes de IA
Resumen
Cómo forzar JSON válido en un agente sin reintentos ni regex: structured outputs de OpenAI (json_schema + strict), Gemini (response_format con JSON Schema) y Claude (output_config.format). Tabla JSON mode vs schema, límites oficiales, Zod/Pydantic y cuándo no basta el prompt.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente que “promete JSON” y a veces devuelve un bloque de markdown, una coma de más o un campo inventado no es un agente: es una fuente de retries. Structured outputs (salidas estructuradas) es la API que obliga al modelo a respetar un JSON Schema. No es un prompt más agresivo. Es constrained decoding: el runtime solo deja emitir tokens que siguen el esquema.
Esta guía cubre el contrato real de las tres APIs grandes (OpenAI, Gemini, Claude), cuándo usar schema frente a JSON mode, y el mínimo de código para que un agente extraiga datos o llame tools sin parsear a ciegas. Las fuentes oficiales se verificaron el 3 de septiembre de 2026.
Si todavía no tienes tools con contratos estrictos, lee primero function calling para agentes confiables. Structured outputs es el mismo principio aplicado a la respuesta del modelo, no solo a los argumentos de una herramienta.
Qué problema resuelve (y cuál no)
Sin schema, incluso con “responde SOLO en JSON”, aparecen cuatro fallos repetidos:
- JSON inválido (
JSON.parseexplota). - Falta un campo required.
- Un enum inventado (
status: "kinda-ok"). - Tipos mezclados (
amount: "12.00"cuando pediste number).
Structured outputs elimina 1–4 a nivel de decodificación. No elimina alucinaciones de contenido: si el schema pide email y el texto no tiene email, el modelo puede rellenar un string vacío o un valor plausible. El schema garantiza forma, no verdad. Para verdad necesitas evals sobre casos reales.

OpenAI: json_schema no es JSON mode
OpenAI documenta dos formatos de texto. No los mezcles:
Structured Outputs (json_schema) | JSON mode (json_object) | |
|---|---|---|
| Garantía | Adhiere a tu JSON Schema | Solo “es un objeto JSON” |
| Campos required | Sí, si el schema los marca | No |
| Enums | No puede inventar valores fuera del enum | Puede |
| Cómo se activa | text.format con type: "json_schema" y strict: true | text.format con type: "json_object" |
| Modelos (docs oficiales) | gpt-4o-mini, gpt-4o-2024-08-06 y posteriores | gpt-3.5-turbo, gpt-4-*, gpt-4o-* |
| Recomendación oficial | Usarlo siempre que el modelo lo soporte | Fallback si el modelo no soporta schema |
El SDK de Python/JS evita escribir el schema a mano: defines un modelo Pydantic o un objeto Zod y llamas responses.parse. El resultado llega en output_parsed, ya tipado. Ejemplo mínimo (Python):
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class Ticket(BaseModel):
customer: str
intent: str
priority: str
response = client.responses.parse(
model="gpt-4o-mini",
input=[
{"role": "system", "content": "Extrae el ticket. No inventes campos."},
{"role": "user", "content": "Ana no puede entrar y dice que es urgente."},
],
text_format=Ticket,
)
ticket = response.output_parsed
Dos reglas de schema que OpenAI exige y que rompen builds si las ignoras:
- En cada objeto,
additionalPropertiesdebe serfalse. - Límites: hasta 5.000 propiedades en total, 10 niveles de anidación, 1.000 valores de enum en todo el schema, y no más de 120.000 caracteres sumando nombres de propiedades, definiciones, enums y consts.
Structured outputs también aparece en function calling: el mismo mecanismo valida los argumentos de la tool. Si tu agente solo llama tools, activa schema ahí; si necesita devolver un objeto al caller (extracción, clasificación, reporte), usa text.format / text_format.
OpenAI documenta un extra útil: los refusals de seguridad son detectables en el objeto de respuesta, no se disfrazan de JSON a medias.
Gemini: response_format con JSON Schema
Gemini documenta structured outputs como “el modelo adhiere a un JSON Schema que tú das”. El caso de uso oficial es el mismo que el de un agente: extraer datos, clasificar con enums y producir inputs para tools o APIs.
En el SDK actual el contrato visible en la documentación es response_format con mime_type: "application/json" y un schema (por ejemplo Recipe.model_json_schema() en Pydantic). En JavaScript el schema se declara como objeto JSON Schema o vía Zod. Tipos que sí cubre la guía oficial: object, array, string, integer, enum, anyOf, format (date-time, date, time) y additionalProperties.
Patrón práctico para un agente:
- Declara el schema en código (Pydantic/Zod), no en un string suelto.
- Pide
application/json. - Valida el texto de salida con el mismo modelo (
model_validate_json). Si el SDK ya parsea, usa ese objeto y no vuelvas aJSON.parsea mano.
Gemini también documenta anyOf para salidas condicionales (por ejemplo un clasificador de spam cuyo payload cambia según la categoría). Eso es más limpio que un schema gigante con 20 campos opcionales.
Claude: output_config.format + strict en tools
Claude separa dos piezas que conviene entender:
- JSON outputs (
output_config.format): la respuesta de texto es JSON válido según tu schema. Sirve para extraer, reportar o devolver un objeto a tu app. - Strict tool use (
strict: true): valida nombre y argumentos de cada tool.
Se pueden usar juntas en el mismo request. El parámetro viejo output_format y el header beta structured-outputs-2025-11-13 siguen aceptados un tiempo, pero el SDK de Python v1.0 ya no acepta output_format={...} en client.beta.messages.create(): hay que pasar output_config. Si copias un snippet de 2025, esa es la razón del TypeError.
El argumento de Claude es el mismo constrained decoding: sin esto ves JSON malformado, campos missing y tipos inconsistentes; con esto no hace falta reintentar por schema.
Cómo elegir (árbol corto)
- ¿El modelo debe devolver un objeto a tu código? Schema en la respuesta (
json_schema/response_format/output_config.format). - ¿El modelo debe llamar una tool tuya? Schema en la tool (
strict/ function calling con structured outputs). A menudo necesitas ambos. - ¿El modelo no soporta schema? JSON mode o “responde JSON” +
JSON.parse+ retry. Trátalo como deuda: peor latencia, peor costo, peor tasa de éxito. - ¿El schema cambia por request? Genera el schema en código a partir de Pydantic/Zod. No concatenes JSON Schema a mano en el prompt.

Checklist para un agente en producción
- El schema vive en código (Pydantic o Zod), no en el prompt.
-
additionalProperties: falseen cada objeto (OpenAI lo exige; en los demás evita campos basura). - Enums cerrados para estados, categorías e intents. Nada de
stringlibre donde hay 5 valores reales. - Un campo
confidenceoneeds_humansi el agente puede no saber. El schema no inventa verdad; deja una vía de escape. - Parseas con el SDK (
output_parsed/model_validate_json), no con regex ni con “busca el primer{”. - Logs del objeto parseado, no del texto crudo, para observabilidad.
- Un eval de 20 ejemplos: JSON válido no es suficiente si el
intentsale mal. - Límites de schema revisados (anidación, enums, tamaño). Un schema de 15 niveles no va a pasar.
Errores que siguen siendo tuyos
Structured outputs no sustituye defensas de prompt injection: un atacante puede meter texto que el schema acepta (note: "ignora las reglas"). El schema no es un permission layer.
Tampoco sustituye tools bien diseñadas. Un schema run(instruction: string) es tan peligroso como la tool equivalente. Campos estrechos, acciones pequeñas, validación en tu código después del parse.
Y no uses structured outputs para prosa larga (artículos, mails). El schema brilla en datos: tickets, extracciones, clasificaciones, planes de pasos, argumentos de API. Para texto libre, deja el formato en texto.
FAQ
¿Puedo definir el schema solo en el system prompt? Puedes, y fallará en el 5–15 % de los casos según el modelo y la complejidad. El punto de esta API es no depender de eso.
¿Zod o JSON Schema a mano? Zod/Pydantic. El schema generado es lo que viaja por la API; tu código comparte un solo tipo entre request y parse.
¿Qué hago si el modelo se niega? En OpenAI el refusal es un estado de la respuesta, no un JSON a medias. No lo trates como parse error: rutea a humano o a un fallback.
¿Funciona con streaming? Depende del proveedor y del endpoint. Si streameas, no consumas el JSON hasta el evento final o usa el parser del SDK. Un objeto a medias no es un objeto.
Empieza por un schema de 4–6 campos sobre un flujo real (ticket, lead, extracción de factura). Cuando eso pase evals, recién anidas. El schema más útil es el más corto que tu código ya sabe usar.
Lecturas relacionadas
Sigue explorando Herramientas y otras piezas para builders.

Max turns y recursion_limit: cómo cortar un agente que no para

Truncar tool results en agentes: recortar el payload, no el contrato

Tool calling en paralelo: cuándo sí, cuándo no y cómo devolver resultados
