Ir al contenido principal
Laravel, thinking fast.

En el capítulo anterior el contrato del driver se describía como de transporte. Esto es lo que significa en la práctica:

$content = (string) $data->json('choices.0.message.content');
$result = json_decode($content, true);

if (! is_array($result) || $result === []) {
    throw new \RuntimeException(
        'AI site content response could not be decoded into a non-empty array.'
    );
}

/** @var array<string, mixed> $result */
return $result;

El driver promete exactamente una cosa: lo que sale de aquí es un array decodificado y no vacío. No que sea útil. No que esté completo. Decodificable y no vacío.

Esa es una promesa pequeña, y es del tamaño correcto, porque el driver no tiene ni idea de qué quería el llamante. No sabe qué categoría de contenido se pidió, qué claves necesita la plantilla ni qué escribió el usuario. Validar el significado aquí obligaría a enseñarle al driver todas las funciones que lo usan, y entonces habría que editarlo cada vez que una función cambia de opinión.

Haz la promesa pequeña, hazla absoluta y pon el criterio en otro sitio.

JSON válido que te arruina la tarde

Este es el fallo que nos enseñó la diferencia, y merece recorrerlo despacio porque todo en él tenía buena pinta.

El modo de salida estructurada estaba activo. La respuesta se analizó. El array no estaba vacío. El driver lo devolvió, el DTO lo aceptó, el resource lo serializó y el servicio de contenido lo confirmó en el repositorio del sitio.

El modelo había omitido hero.title.

No saltó nada. El DTO tenía un campo que admitía null, cómo no —algunos sitios no tienen subtítulo, así que en algún punto de esa cadena admitir null era razonable. Para cuando la omisión importó, ya había atravesado cuatro capas, cada una de las cuales lo consideró problema de otro, y lo que se confirmó en un repositorio real fue un fichero de contenido cuya sección hero resolvía a un literal return null;.

El sitio se renderizó. Renderizó un hero en blanco.

Nadie se equivocó por separado. El proveedor cumplió su contrato: el JSON era válido. El driver cumplió el suyo: el array se decodificaba y no estaba vacío. El DTO cumplió el suyo: el campo admitía null. La brecha estaba en que a ninguna capa se le había dicho nunca cómo es una respuesta completa —y una cadena de decisiones permisivas razonables por separado produce un sistema sin ninguna opinión.