Pídele a un modelo que responda en JSON y la mayoría de las veces
json.loads() funciona sin problemas. Los fallos son los que te cuestan una
sesión de debugging: un comentario de una línea dentro del objeto, un code
fence de markdown envolviéndolo, una clave escrita userEmail cuando tu
código espera user_email. Un prompt más estricto (“responde SOLO con el
objeto JSON, sin ningún otro texto”) hace que estos casos sean más raros. No
los hace imposibles, porque el modelo puede ignorar la petición igual. La
salida estructurada elimina la petición de la ecuación: le entregas a la API
un JSON Schema, y este restringe el muestreo de tokens del propio modelo para
que la respuesta tenga que ajustarse a él.
JSON mode frente a salida estructurada: qué garantiza cada uno
El “JSON mode” (response_format: {"type": "json_object"} en la API antigua
de OpenAI, y el equivalente en la mayoría de otros proveedores) garantiza una
sola cosa: la salida se puede parsear como JSON. No dice nada sobre qué
claves existen, qué tipo tiene cada una, o si un campo del que dependes está
presente. Un modelo en JSON mode puede devolver {"result": "yes"} cuando tu
código espera {"status": "approved", "confidence": 0.91}, y nada te avisa
de que algo salió mal.
La salida estructurada (OpenAI la llama Structured Outputs, Anthropic la llama JSON outputs) es una garantía distinta: la respuesta coincide con el JSON Schema que proporcionas, con las claves requeridas presentes y el tipo correcto en cada una. El function calling resuelve un problema adyacente: que el modelo le pida a tu código que ejecute algo. Los dos se confunden con facilidad porque ambos pasan un esquema. Lo que cambia es qué moldea ese esquema:
| Función | Qué garantiza | Para qué sirve |
|---|---|---|
JSON mode (json_object) | Solo sintaxis JSON válida | Fallback heredado, sin esquema que exigir |
| Salida estructurada (esquema strict) | El JSON coincide exactamente con tu esquema | Dar forma a la respuesta para el usuario o tu código |
| Function calling / tool use | El nombre y los argumentos del tool coinciden con su esquema | El modelo decide invocar tu código |
Un pipeline en producción suele usar las dos últimas juntas. El tool use strict evita que las llamadas de función de un agente lleguen malformadas, y un esquema strict en la respuesta final tipa el resumen que se devuelve al usuario.
Cómo el constrained decoding fuerza la coincidencia
El modelo nunca compara su respuesta con tu esquema después de generarla. El esquema cambia lo que tiene permitido generar, un token a la vez.
La capa de serving compila tu JSON Schema en una gramática: una máquina de estados que registra, en cada posición de la salida, qué tokens todavía llevarían a algo válido. Antes de muestrear cada token, todo token del vocabulario que la gramática descarta en esa posición ve su logit empujado a infinito negativo, así que tiene probabilidad cero tras el softmax. El modelo muestrea de lo que queda, y las probabilidades relativas entre los tokens que sobreviven se mantienen. Una clave mal escrita, una llave de cierre en el lugar equivocado, una cadena donde el esquema exige un número: en cuanto la gramática los prohíbe, esos tokens dejan de ser opciones.
Esa fase de compilación tiene un coste. Construir una gramática para un esquema nuevo requiere trabajo real, así que la primera petición contra ese esquema es más lenta que las siguientes. La API de Anthropic guarda en caché la gramática compilada durante 24 horas desde su último uso, y las llamadas posteriores con el mismo esquema se saltan la compilación.
El mecanismo también explica el límite. El constrained decoding fija solo la
forma, nada más. La gramática sabe qué tokens son sintácticamente legales,
no cuál es verdadero, así que el modelo puede seguir rellenando un campo
"email" válido con una dirección inventada.
Cómo obtener JSON válido según el esquema en OpenAI, Claude y Ollama
La Responses API de OpenAI recibe el esquema en text.format, con strict
activado:
| |
Claude recibe el esquema en output_config.format, no en una definición de
tool:
| |
El JSON vuelve en el contenido de texto de la respuesta, no en un bloque de tool call. El workaround antiguo (definir un tool falso, forzar al modelo a llamarlo, leer los argumentos) todavía funciona en modelos que solo soportan tool use. Donde existe salida estructurada nativa no lo necesitas: el decoder hace cumplir el esquema directamente, en lugar de confiar en que los argumentos del tool coincidan por casualidad.
Un modelo ejecutado en local con Ollama
recibe el esquema a través del campo format de /api/chat:
| |
El mecanismo es el mismo. El runtime llama.cpp de Ollama construye una
gramática a partir del esquema y enmascara los tokens igual que las API
alojadas.
Qué funciones de JSON Schema soportan los proveedores
Cada proveedor hace cumplir un subconjunto de JSON Schema, y los subconjuntos difieren lo bastante como para romper un esquema copiado de una API a otra.
OpenAI exige que todo objeto lleve "additionalProperties": false con cada
una de sus propiedades listadas en required — no hay forma de marcar un
campo como realmente opcional, así que modélalo como required y nullable.
Anthropic es más permisivo: una propiedad que no esté en required es
genuinamente opcional, y las propiedades required aparecen primero en la
salida, seguidas de las opcionales que estén presentes. A partir de ahí,
ambos difieren más. Anthropic soporta $ref/$defs locales, anyOf y
const, pero rechaza esquemas recursivos, límites numéricos como minimum y
maximum, y restricciones de longitud de cadena como maxLength. Los
ejemplos de OpenAI incluyen un $ref: "#" recursivo para salidas en árbol,
algo que la documentación de Anthropic descarta explícitamente.
Cuando tu esquema necesita una restricción que el proveedor no soporta,
muévela a la description del campo (“debe ser un entero positivo menor que
100”) y compruébala tú mismo después del parseo. Los helpers de los SDK en
ambas plataformas hacen esto automáticamente cuando construyes el esquema a
partir de un modelo de Pydantic o Zod en vez de escribir JSON Schema a mano.
Cómo validar la salida estructurada después del parseo
Una forma válida no es una respuesta correcta. El esquema garantiza que
severity sea uno de tus cuatro valores del enum. Si “blocking” es la
decisión correcta para este ticket es una pregunta distinta. Trata el objeto
parseado como tratarías el envío de un formulario: input que validar, no
verdad establecida.
| |
Instructor envuelve OpenAI, Anthropic y
la mayoría de otros proveedores en un ciclo de validación y reintento. Le
pasas un modelo de Pydantic. Ante un fallo de validación, devuelve el error
al modelo y pregunta de nuevo, hasta un límite de reintentos que tú fijas, en
lugar de lanzar un ValidationError al primer fallo. También es la vía
práctica para conseguir una garantía de esquema en un proveedor o un modelo
antiguo sin strict mode nativo: el ciclo de reintento sustituye a la
validación que haría el decoder, y cada fallo cuesta un viaje de ida y vuelta
extra.
Los validadores de campo detectan valores malformados en una sola respuesta. Para comprobar si las respuestas son correctas en cientos de entradas, y si un cambio de prompt las empeoró, necesitas un eval suite.
Cuándo la salida estructurada falla igual
El strict mode no cubre estos casos. Compruébalos antes de ponerte a buscar un bug en tu propio código:
- Refusals. Un modelo puede negarse a responder, y el texto del rechazo
no coincide con tu esquema. Comprueba
stop_reason(o el equivalente) antes de parsear, no después de que el parseo lance un error. - Truncamiento. Llegar a
max_tokensa mitad de un objeto produce JSON incompleto. La gramática controla qué token viene después, no cuánto espacio queda para terminar. Sube el límite o reduce el esquema. - Rutas sin forzar. JSON mode, una librería de reintento sobre un modelo
sin strict mode, o un proveedor que ignora en silencio una palabra clave
del esquema que no soporta pueden devolver
"Blocking"cuando tu enum dice"blocking". Si alguna petición de tu sistema toma una de esas rutas, normaliza los valores al entrar. - Tamaño del esquema. Un esquema grande con muchas ramas
anyOfañade tokens a cada petición, además del retraso de compilación de un esquema nuevo. Mantén los esquemas tan planos y pequeños como lo permita la tarea.
La garantía del proveedor es real, pero tu código de parseo no debería asumir que se cumplió.
JSON mode, esquema strict o function calling: cuál usar
Usa el JSON mode plano solo contra un modelo o una ruta de SDK sin soporte de esquemas, como fallback y no como opción por defecto. Usa un JSON Schema strict cuando estés dando forma a una respuesta para tu propio código o un usuario: extracción, clasificación, un resumen tipado. Usa el function calling cuando sea el modelo quien decida si ejecutar algo. Activa el strict mode también en la definición del tool, para que una llamada malformada falle en la API y no dentro de tu código.
Si todo lo que necesitas es una etiqueta de un enum corto, compara también un modelo system one. Cuando la salida está restringida a un conjunto conocido de respuestas, un modelo puede elegir una en lugar de generarla, y las concesiones de latencia y coste difieren lo bastante como para medirlas en tu propio tráfico.
Para todo lo demás, la opción por defecto es un esquema strict más un validador sobre el resultado parseado. El esquema mantiene correcta la forma. Comprobar que el contenido es correcto sigue siendo tarea tuya.