Schema validation en Postman: un contrato formal para la API

Definí un JSON Schema para Customer List, validé con Ajv en Postman. Rompí el schema a propósito para probar que detecta errores reales. Proceso completo.

Postman Runner showing 11 passed 1 failed with Console displaying three schema errors summarized with occurrence count and example paths
Tres errores a propósito, tres líneas en el resumen. TotalCount (1x), Region (60x), Country (91x). El schema los atrapó a todos.

Nota para el lector

Mis assertions validaban puntos sueltos. El schema valida la estructura completa. Cuando algo cambia, falla. Esa es la diferencia.

Este post es parte de la serie de API Testing con Postman sobre Serenity Demo https://demo.serenity.is/Account/Login/?ReturnUrl=%2F. Los posts anteriores cubren el análisis con DevTools, la construcción de requests, el manejo del CSRF token, assertions con pm.test, testing negativo del login, el debugging del Runner, y CRUD completo.

Contexto

Hasta ahora, las assertions en Customer List validaban cosas puntuales:

pm.test("Status code is 200")
pm.test("Response is JSON")
pm.test("Response has Entities array")
pm.test("TotalCount is 91")
pm.test("Each entity has CustomerID and CompanyName")

Cinco assertions. Todas pasan. Todo verde.

Pero hay un problema: si mañana la API cambia CustomerID de string a number, o elimina el campo ContactName, o agrega un typo como CustmerID — mis tests no se enteran. Siguen validando status 200, TotalCount 91, todo pasa verde. Mientras tanto, el frontend se rompe.

La validación puntual no alcanza. Se necesita validar el contrato entero.

Qué es schema validation

Schema validation es definir "cómo debería verse" una response y verificar automáticamente que la response real cumpla.

Pensalo como un plano de una casa. Mis assertions anteriores eran como revisar: "¿tiene puerta?", "¿tiene techo?", "¿tiene piso?". Una por una. Schema validation es tener el plano completo y comparar de una sola vez: "¿esta casa cumple con el plano?". Si alguien sacó una pared o cambió una ventana por una puerta, el schema lo detecta.

En la práctica, un JSON Schema define:

  • Qué campos debe tener la response (obligatorios y opcionales)
  • Qué tipo de dato tiene cada campo (string, integer, array)
  • Qué campos NO deberían aparecer (campos inesperados)

Esto es la base de contract testing: verificar que la API cumple con el contrato acordado con los consumidores.

Analizar la response antes de escribir el schema

Antes de armar el schema, necesitaba entender la estructura real. Corrí Customer List y analicé la response completa (91 registros).

Nivel raíz:

{
    "Entities": [...],
    "TotalCount": 91,
    "Skip": 0,
    "Take": 100
}

Cuatro campos. Entities es un array de objetos, los otros tres son integers.

Cada entidad tiene esta estructura:

{
    "CustomerID": "ALFKI",
    "CompanyName": "Alfreds Futterkiste",
    "ContactName": "Maria Anders",
    "ContactTitle": "Sales Representative",
    "Address": "Obere Str. 57",
    "City": "Berlin",
    "PostalCode": "12209",
    "Country": "Germany",
    "Phone": "030-0074321",
    "Fax": "030-0076545"
}

Pero no todos los registros se ven así. Al revisar los 91 registros descubrí algo:

Campos que siempre están presentes: CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Country, Phone

Campos que a veces faltan:

  • Region — solo aparece en clientes de USA, Canadá, Brasil, Venezuela y algunos otros. Los de Europa no lo tienen.
  • Fax — muchos clientes no tienen fax.
  • PostalCode — la mayoría tiene, pero al menos uno no (HUNGO, Cork, Ireland).

Y lo clave: cuando un campo está vacío, la API no manda el campo. No manda null, no manda "". Directamente lo omite. ANTON no tiene Fax, y el campo ni aparece en el JSON.

Postman Runner with all 12 tests passed and response panel showing ANTON customer record without Fax field highlighted
ANTON no tiene Fax. El campo no aparece en el JSON — la API lo omite en vez de mandar null. Por eso Fax no va en required.

Eso define qué va en required y qué no.

El schema

const customerSchema = {
    type: "object",
    required: ["Entities", "TotalCount", "Skip", "Take"],
    properties: {
        TotalCount: { type: "integer" },
        Skip: { type: "integer" },
        Take: { type: "integer" },
        Entities: {
            type: "array",
            items: {
                type: "object",
                required: ["CustomerID", "CompanyName", "ContactName",
                           "ContactTitle", "Address", "City",
                           "Country", "Phone"],
                properties: {
                    CustomerID: { type: "string" },
                    CompanyName: { type: "string" },
                    ContactName: { type: "string" },
                    ContactTitle: { type: "string" },
                    Address: { type: "string" },
                    City: { type: "string" },
                    Region: { type: "string" },
                    PostalCode: { type: "string" },
                    Country: { type: "string" },
                    Phone: { type: "string" },
                    Fax: { type: "string" }
                },
                additionalProperties: false
            }
        }
    },
    additionalProperties: false
};
Postman Scripts tab showing complete JSON Schema definition with Ajv for Customer List including required fields and additionalProperties false
El schema completo: 8 campos obligatorios, 3 opcionales (Region, PostalCode, Fax), additionalProperties false, allErrors true.

Puntos a notar:

required solo tiene 8 campos. Region, PostalCode y Fax no están porque la API los omite en algunos registros. Si los pongo como required, el schema falla en el primer cliente que no los tenga.

additionalProperties: false dice: "estos son los ÚNICOS campos permitidos". Si mañana desarrollo agrega un campo Email sin avisar, el schema falla. Es como un guardia que dice "solo pueden entrar los que están en la lista".

Todos los tipos son string excepto TotalCount, Skip y Take que son integer. PostalCode es string aunque parezca número — hay códigos como "S-958 22", "WA1 1DP", "05021".

La herramienta: Ajv

Postman trae Ajv (Another JSON Validator) integrado. No hay que instalar nada. Una línea:

const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });

allErrors: true es importante. Sin eso, Ajv frena en el primer error que encuentra. Con allErrors, reporta todos.

El test

El script completo en el tab Post-response de Customer List:

// Schema validation con Ajv
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });

const customerSchema = {
    // ... (schema completo de arriba)
};

const jsonData = pm.response.json();
const validate = ajv.compile(customerSchema);
const valid = validate(jsonData);

pm.test("Response matches Customer List schema", function () {
    if (!valid) {
        // Resumir errores por tipo
        const summary = {};
        validate.errors.forEach(err => {
            const key = err.schemaPath + " | " + err.message;
            if (!summary[key]) {
                summary[key] = { count: 0, example: err.dataPath };
            }
            summary[key].count++;
        });

        console.log("Schema errors (resumen):");
        Object.keys(summary).forEach(key => {
            console.log(`  ${key} (${summary[key].count}x) — ejemplo: ${summary[key].example}`);
        });
    }
    pm.expect(valid).to.be.true;
});

Resultado: 12/12 tests passed. El schema valida la response completa.

Postman Runner results showing all 12 tests passed including Response matches Customer List schema as sixth assertion on Customer List request
12 de 12. Los 6 tests de Customer List: status, JSON, Entities, TotalCount, campos obligatorios y schema validation.

Romper para probar

Un schema que pasa en verde no demuestra nada si no verifico que también falla cuando debe. Lo rompí a propósito de tres formas.

Prueba 1: Agregar Region como obligatorio

Agregué "Region" al array de required en la entidad. Region solo aparece en algunos clientes (USA, Canadá, Brasil). Los de Europa no lo tienen.

Resultado: FAIL.

La Console mostró:

keyword: "required"
dataPath: ".Entities[0]"
missingProperty: "Region"
message: "should have required property 'Region'"

Entities[0] es ALFKI (Alfreds Futterkiste, Alemania). No tiene Region. El schema lo atrapó.

Postman Runner showing schema validation fail with Console error indicating 60 entities missing required Region property starting at Entities zero
Region como required: 60 de 91 clientes no lo tienen. El schema confirma que es un campo opcional, no obligatorio con datos faltantes.

Fíjate lo que dice: ubicación exacta (.Entities[0]), campo faltante (Region), motivo (required). Si desarrollo decide hacer Region obligatorio pero no actualiza todos los registros, este test lo detecta.

Con allErrors: true, el resumen muestra que no es solo ALFKI: son 60 registros de 91 los que no tienen Region. Eso confirma que Region es claramente un campo opcional, no un campo obligatorio con datos faltantes.

Prueba 2: Cambiar el tipo de TotalCount

Cambié TotalCount de "integer" a "string" en el schema. La API devuelve "TotalCount": 91 (un número), pero el schema ahora espera un string.

Resultado: FAIL.

keyword: "type"
dataPath: ".TotalCount"
message: "should be string"

Detectó que 91 es integer, no string. Si desarrollo cambia un campo de tipo sin avisar, el schema lo atrapa.

Postman Runner showing schema validation fail with Console error indicating TotalCount should be string one occurrence at TotalCount path
TotalCount como string: la API devuelve 91 (integer). El schema detecta el tipo incorrecto en una línea.

A diferencia de la prueba con Region (60 registros sin el campo), acá es 1x porque TotalCount existe una sola vez en la response. El schema detecta ambos escenarios: campos faltantes en entidades repetidas y tipos incorrectos en campos únicos.

Prueba 3: Dos errores al mismo tiempo

Puse TotalCount como "string" y Fax como "integer". ¿Reporta los dos?

Primera vez: solo reportó TotalCount. Ajv por defecto frena en el primer error.

El problema era que estaba usando new Ajv() sin opciones. La solución:

const ajv = new Ajv({ allErrors: true });

Con allErrors: true, corrí de nuevo. Esta vez la Console me inundó: TotalCount una vez, y Fax 69 veces (una por cada cliente que tiene Fax).

Técnicamente correcto. Inútil en la práctica. Nadie quiere leer 69 errores iguales.

Postman Console flooded with raw schema errors showing repeated Fax type validation failures for every entity without error grouping
allErrors sin resumen: cada Fax que no es integer genera su propia línea. Técnicamente correcto, inútil en la práctica.

Prueba 4: Mejorar el log de errores - Varios errores al mismo tiempo

Escribí un resumen que agrupa errores por tipo y cuenta ocurrencias:

if (!valid) {
    const summary = {};
    validate.errors.forEach(err => {
        const key = err.schemaPath + " | " + err.message;
        if (!summary[key]) {
            summary[key] = { count: 0, example: err.dataPath };
        }
        summary[key].count++;
    });

    console.log("Schema errors (resumen):");
    Object.keys(summary).forEach(key => {
        console.log(`  ${key} (${summary[key].count}x) — ejemplo: ${summary[key].example}`);
    });
}

Rompí tres cosas a propósito: TotalCount como "string", Region en required, y Country como "integer". ¿Reporta los tres?

Resultado:

Tres líneas. Cantidad de ocurrencias y limpio.

Postman Runner with 11 passed 1 failed and Console showing three grouped schema errors for TotalCount type Region required and Country type
Tres errores distintos, agrupados. Cada uno con cantidad de ocurrencias y ejemplo. Mucho mejor que las 152 líneas sin resumen.

Un detalle más: el orden del console.log

Al principio tenía el console.log después de pm.expect(valid).to.be.true. El problema: cuando pm.expect falla, corta la ejecución del test. El console.log nunca se ejecutaba.

La solución: poner el log antes del expect. Así, si falla, ya logueó el error.

Schema vs assertions: no se reemplazan

El schema no reemplaza las assertions que ya tenía. Se complementan.

Las assertions validan lógica de negocio: "TotalCount es 91", "la cookie existe". Eso el schema no lo hace — solo valida estructura y tipos.

El schema valida el contrato completo: "la response tiene exactamente estos campos, con estos tipos, sin extras". Eso las assertions individuales no lo cubren.

Los dos juntos son una red de seguridad más completa.

Estado actual

Customer List ahora tiene 6 tests:

  1. Status code is 200
  2. Response is JSON
  3. Response has Entities array
  4. TotalCount is 91
  5. Each entity has CustomerID and CompanyName
  6. Response matches Customer List schema ← nuevo
Postman Collection Runner final results showing all 12 tests passed with schema validation as last test confirming Customer List contract
Estado final: 12 de 12. El schema original pasa. La colección tiene assertions individuales y validación de contrato.

El schema cubre:

  • 4 campos raíz (Entities, TotalCount, Skip, Take) con tipos
  • 11 campos por entidad con tipos
  • 8 campos obligatorios, 3 opcionales (Region, PostalCode, Fax)
  • Bloqueo de campos inesperados (additionalProperties: false)

Total de la colección: 12 tests, todos verdes.

Takeaways

Analizar la data real antes de escribir el schema. No asumir que todos los campos vienen siempre. Los 91 registros me mostraron que Region, Fax y PostalCode son opcionales.

Ajv frena en el primer error por defecto. allErrors: true es necesario para ver el panorama completo.

allErrors puede inundar la Console. Un resumen agrupado por tipo de error es más útil que 69 líneas iguales.

El orden del código importa. console.log antes de pm.expect, no después. Si el expect falla, todo lo que viene después no se ejecuta.

Romper el schema a propósito es tan importante como hacerlo pasar. Un test que siempre pasa en verde no demuestra nada.

Próximo paso

El schema cubre estructura. El siguiente nivel es validar comportamiento con datos variados: data-driven testing con CSV contra el Runner.

Crear un archivo con distintas combinaciones de datos de cliente (válidos, inválidos, edge cases), alimentar el Runner con ese archivo, y validar cada resultado automáticamente.