Structured Outputs con Pydantic

Resumen

Cuando trabajas con la API de OpenAI y necesitas más que texto plano, las salidas estructuradas con Pydantic te permiten renderizar cards, validar datos y guardar resultados de forma confiable. Esta guía es para quienes ya generan respuestas en Markdown y quieren dar el salto a datos con forma predecible en Python o TypeScript.

Por qué el Markdown no basta para estructurar datos

Hasta ahora la respuesta llegaba en formato Markdown, que organiza el texto por defecto pero carece de una estructura confiable. Si solo quieres leer, funciona. Pero cuando necesitas renderizar cards, validar datos o guardar resultados, el texto organizado no alcanza [00:15].

Ahí entra Pydantic, una librería de Python que revisa, corrige y rechaza datos que no encajen en el molde o tipo que defines para ellos. Es como un portero: si el dato no cumple con la estructura esperada, no pasa.

¿Qué es Pydantic? Es una librería de Python que valida datos contra un modelo definido. Revisa los tipos, corrige lo que puede y rechaza lo que no encaja con la estructura que declaraste.

Cómo paso de responses.create a responses.parse

Hasta este punto veníamos usando la Responses API con client.responses.create, que hace el llamado a la API de OpenAI, y luego renderizábamos response.output_text en la terminal o en el navegador. Eso sirve para generación de texto [00:50].

Para una salida estructurada, la documentación de OpenAI recomienda dos cambios clave:

  • Usar parse en lugar de create.
  • Recibir output_parsed en lugar de output_text.
  • Definir el formato de respuesta con tu modelo de Pydantic.

Con esos ajustes, la respuesta deja de ser un bloque de texto y se convierte en un objeto con campos que puedes llamar uno por uno.

Qué necesito si trabajo con TypeScript

Si llegaste hasta aquí trabajando con TypeScript, la documentación de OpenAI también entrega alternativas más allá del ejemplo de Python. Para JavaScript la opción equivalente a Pydantic es Zod, que te permite definir los tipos de datos y estructurar el archivo de la misma forma [01:40].

La idea es la misma en ambos lenguajes: primero declaras la estructura de datos que vas a necesitar y luego pides que la respuesta se ajuste a ella.

Cómo defino los modelos en un archivo models.py

Lo más correcto es generar la estructura de datos en un archivo separado en lugar de dejarla dentro del archivo principal. Para eso creamos un archivo models.py [02:10].

Dentro importamos List desde typing y el BaseModel de Pydantic. Ese BaseModel es el que permite definir clases con campos tipados. Por ejemplo, una clase CampaignBrief que recibe:

  • titulo: un string.
  • objetivo: un string.
  • audiencia: una lista de strings.
  • tono: un string.
  • canales: una lista de strings.
  • next_steps: una lista de strings.

python from typing import List from pydantic import BaseModel

class CampaignBrief(BaseModel): titulo: str objetivo: str audiencia: List[str] tono: str canales: List[str] next_steps: List[str]

Al tenerlo dentro del package llamado app, puedes importarlo en cualquier otro archivo y consumirlo desde ahí.

Cómo genero la función generate_structured_brief

En el archivo de client ya teníamos varias funciones: una para generar texto, otra con chat completions y la de generar el brief. Esa última la dejamos solo a modo de comparación, porque la nueva se llama generate_structured_brief [03:40].

La estructura es bastante parecida: recibe una idea como string, pero setea un tipo que es el CampaignBrief. Las diferencias frente a la versión anterior son puntuales pero decisivas:

  • Se llama a client.responses.parse en lugar de create.
  • Se recibe output_parsed en lugar de output_text.
  • Se define el formato de la respuesta con CampaignBrief.

Para que funcione, importamos el modelo arriba del archivo con from app.models import CampaignBrief. Así el campo del formato y la estructura que devuelve el llamado no quedan en blanco [04:30].

¿Cuál es la diferencia entre create y parse en la API de OpenAI? create devuelve texto libre en output_text. parse valida la respuesta contra un modelo de Pydantic y la entrega ya estructurada en output_parsed, lista para usar campo por campo.

Cómo consumo el brief estructurado en main.py

En main.py eliminamos las pruebas anteriores y dejamos el llamado al OpenAI client, pero ahora invocamos la nueva función generate_structured_brief [05:15].

En el ejemplo se genera un brief para un evento de AI en Medellín. Y aquí viene lo interesante: como la respuesta tiene estructura, no solo imprimes el resultado completo, también puedes llamar cada elemento por separado.

  • El titulo.
  • El objetivo.
  • La audiencia.
  • El tono.
  • Los canales.
  • Los next_steps.

Para que se vea mejor en la terminal, imprimimos un string descriptivo antes de cada campo. Al ejecutar main.py, primero aparece la respuesta completa en un solo bloque, porque el primer print pide el resultado directo, y luego cada parámetro separado y ordenado [06:20].

Esto hace todo mucho más fácil de manejar: puedes llamar específicamente al título y consumirlo dentro de una aplicación o darle forma en una interfaz.

¿Para qué sirve una salida estructurada en una app? Te permite acceder a cada dato de forma individual, como el título o los canales, para renderizarlo en una interfaz, validarlo o guardarlo, sin tener que parsear texto plano manualmente.

Con la función lista, el siguiente paso es extender la interfaz para que consuma la data estructurada directamente desde la UI y no solo desde la terminal. Tu aplicación evoluciona así de simple texto plano a datos con una estructura técnica sólida.

Muéstrame en la sección de comentarios qué resultado obtuviste al agregar esta función y generar el brief estructurado desde la interfaz.