Salidas estructuradas: datos confiables de la IA (JSON)
Hacé que la IA devuelva siempre el mismo JSON, listo para entrar a tu sistema sin parsear prosa ni corregir la salida a mano.
Cuando le pedís algo a la IA y la respuesta la va a leer una persona, alcanza con que escriba bien. Pero si esa respuesta tiene que entrar a otro sistema —tu base de datos, una API, un flujo automatizado— la prosa no te sirve: necesitás datos con una forma fija, siempre igual. Las salidas estructuradas son justamente eso: obligar al modelo a devolver un JSON que respeta un contrato que vos definís. Nada de parsear texto libre, adivinar dónde está el dato ni corregir a mano cuando el modelo se pone creativo.
Por qué querés datos y no prosa
El error clásico es tratar a la IA como si fuera una persona que te pasa la data por chat: le pedís "dame los datos del cliente" y después tu código intenta pescar el nombre y el mail de un párrafo. Funciona en la demo y explota en producción, porque el modelo un día te contesta "Claro, acá tenés:", otro día mete el JSON dentro de un bloque de markdown, y otro día te cambia total por monto_total. Si la salida de la IA va a entrar a otro sistema, tenés que garantizar la forma, no confiar en que "casi siempre" sale bien.
La buena noticia: hoy no hace falta rezarle al prompt. Hay una escala de enfoques, de más frágil a más confiable.
Los enfoques, del más frágil al más confiable
Ordenados de peor a mejor según cuánta garantía te dan:
- Pedir "devolvé JSON" en el prompt. Lo más frágil. El modelo casi siempre obedece, pero "casi" no es un contrato.
- JSON mode. Le garantizás que la salida es JSON válido (parsea sin romperse), pero no que tenga los campos que vos querés.
- Structured Outputs con schema. Le pasás un JSON Schema y el modelo queda obligado a respetarlo: los campos, los tipos y los valores permitidos. Esto lo tenés en OpenAI y en Anthropic (Claude).
- Tool / function calling. El modelo "llama" a una función tuya y los argumentos vienen ya validados contra el schema de esa función. Ideal cuando la IA además tiene que decidir qué acción ejecutar.
Cuanto más arriba en esta escala, menos código defensivo tenés que escribir después. Esto es lo que puede volver del enfoque frágil y romperte el parser:
Pedís: "Devolveme los datos en JSON."
Y a veces te llega:
"Claro, acá tenés:" { 'nombre': 'Ana', } → texto de más, comillas simples y una coma que sobra
el objeto envuelto en un bloque de markdown → tu parser recibe backticks, no JSON
{ "monto_total": 500 } → cambió el nombre del campo que esperabasLos enfoques 3 y 4 hacen que ese problema desaparezca de raíz.
Cómo definís un JSON Schema
Un JSON Schema es la descripción de la forma que tiene que tener la salida. Cuatro piezas te sacan el 90% de las sorpresas:
- Campos y tipos: cada propiedad con su tipo (
string,integer,number,boolean,array,object). required: la lista de campos que el modelo está obligado a devolver. Sin esto, un campo "opcional" puede faltar cuando menos lo esperás.enum: para valores cerrados. Si un campo solo puede serpendiente,pagadaovencida, ponelo como enum y el modelo no puede inventar un cuarto valor.additionalProperties: false: prohíbe campos de más. El modelo devuelve exactamente lo que pediste, ni uno más.
Así se ve un schema real para extraer los datos de una factura:
{
"name": "datos_factura",
"strict": true,
"schema": {
"type": "object",
"properties": {
"proveedor": { "type": "string" },
"numero_factura": { "type": "string" },
"fecha": { "type": "string" },
"moneda": { "type": "string", "enum": ["ARS", "USD", "EUR"] },
"total": { "type": "number" },
"estado": { "type": "string", "enum": ["pendiente", "pagada", "vencida"] },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"descripcion": { "type": "string" },
"cantidad": { "type": "integer" },
"precio_unitario": { "type": "number" }
},
"required": ["descripcion", "cantidad", "precio_unitario"],
"additionalProperties": false
}
}
},
"required": ["proveedor", "numero_factura", "fecha", "moneda", "total", "estado", "items"],
"additionalProperties": false
}
}required + enum + additionalProperties: false son los tres que convierten "más o menos" en "siempre igual". Con strict: true le decís al modelo que el schema no se negocia.
De un mail a un JSON fijo: ejemplo de punta a punta
El caso más útil en la práctica: agarrás un texto libre —un mail, una factura, un mensaje de WhatsApp— y lo convertís siempre en el mismo JSON. Usando el schema de arriba, este es el prompt:
Extraé los datos de esta factura y devolvelos según el schema. Si un dato no aparece en el texto, no lo inventes. Texto: "Factura A-0042 de Insumos del Sur. Fecha 08/07/2026. Les mando 3 resmas de papel a $4.500 c/u y 2 cartuchos de tóner a $28.000 c/u. Total ARS 69.500, vence en 30 días."
Y esto es lo que devuelve, con la forma garantizada por el schema:
{
"proveedor": "Insumos del Sur",
"numero_factura": "A-0042",
"fecha": "2026-07-08",
"moneda": "ARS",
"total": 69500,
"estado": "pendiente",
"items": [
{ "descripcion": "Resma de papel", "cantidad": 3, "precio_unitario": 4500 },
{ "descripcion": "Cartucho de tóner", "cantidad": 2, "precio_unitario": 28000 }
]
}Le pasás diez facturas escritas de diez formas distintas y las diez salen con esta misma estructura. Ese JSON entra derecho a tu base o a tu API: cero parseo de prosa.
Validá la salida antes de confiar en ella
Aunque el modelo garantice el schema, en tu código validá igual. Es tu red de seguridad ante un cambio de versión, un timeout que te devuelve algo raro o un caso borde. En Python se hace con Pydantic:
from pydantic import BaseModel
from typing import Literal
class Item(BaseModel):
descripcion: str
cantidad: int
precio_unitario: float
class Factura(BaseModel):
proveedor: str
numero_factura: str
fecha: str
moneda: Literal["ARS", "USD", "EUR"]
total: float
estado: Literal["pendiente", "pagada", "vencida"]
items: list[Item]
# Si esto no lanza una excepción, la salida es confiable.
factura = Factura.model_validate_json(respuesta_del_modelo)En JavaScript o TypeScript el equivalente es Zod:
import { z } from "zod";
const Factura = z.object({
proveedor: z.string(),
moneda: z.enum(["ARS", "USD", "EUR"]),
total: z.number(),
estado: z.enum(["pendiente", "pagada", "vencida"]),
});
const factura = Factura.parse(JSON.parse(respuestaDelModelo));Definís el schema una sola vez y lo usás para las dos cosas: para pedirle la salida al modelo y para validarla al recibirla. Una fuente de verdad, sin duplicar.
Cuándo usarlo y cuándo no
Usalo cuando la salida es un dato que va a otro lado:
- Extraer campos de textos libres (mails, facturas, formularios, tickets).
- Clasificar en categorías cerradas (usá
enum). - Alimentar una base, una API o un paso siguiente del pipeline.
- Que la IA elija una acción y sus parámetros (ahí va tool calling).
No lo fuerces cuando lo que querés es justamente texto para una persona:
- Redacción, respuestas de chat, resúmenes para leer, contenido creativo.
- Prototipos de un solo uso donde armar el schema cuesta más de lo que rinde.
Regla corta: si el resultado lo lee tu código, estructurá; si lo lee un humano, dejá que escriba. Meter todo en JSON cuando querías un párrafo solo te complica la vida.
Próximo paso
Agarrá una tarea donde hoy copiás y pegás datos a mano desde mails, PDFs o mensajes, y definí el JSON Schema de lo que necesitás que salga. Con eso ya podés pasar de "la IA me tira un texto que después ordeno yo" a "la IA me devuelve el dato listo para usar". Si querés que armemos juntos el schema y el flujo sobre tu caso real, agendá una llamada en /agenda y lo dejamos funcionando sobre tus datos.
¿Querés implementar esto sobre tu caso real? Copiá la guía y pegala en tu agente — o trabajemos juntos.