JSON Toolkit

Puntos clave para diseñar contratos de datos de API JSON

En resumen: Seis cosas son muy difíciles de cambiar una vez publicadas: los envoltorios de respuesta, los formatos de error, la forma de la paginación, la representación de IDs y tiempo, la semántica de null frente a ausente, y la evolución de versiones.

Intermedio~13 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Portada de Puntos clave para diseñar contratos de datos de API JSON

Conclusión: fija en el contrato el envoltorio de respuesta, el formato de error, la representación de IDs y tiempo, y la semántica de null frente a ausente antes de debatir detalles de paginación y nomenclatura; envuelve siempre el nivel superior en un envoltorio data para que la paginación y los IDs de trazabilidad puedan añadirse después sin romper a los consumidores existentes.

En el diseño de API no hay respuestas estándar, pero sí algunas decisiones "difíciles de retirar una vez tomadas" que conviene pensar antes de escribir la primera línea de código.

1. Envolver la respuesta o no

Tres estilos:

// Estilo A: respuesta desnuda
{ "id": 1, "name": "张三" }

// Estilo B: envoltorio data
{ "data": { "id": 1, "name": "张三" } }

// Estilo C: data + metadatos
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Estilo Ventajas Inconvenientes
Respuesta desnuda Lo más simple, una capa menos para el frontend No se pueden adjuntar metadatos; un array en el nivel superior acarrea riesgos de secuestro de JSON y de evolución
Envoltorio data Fácil añadir meta / errors / links Un nivel más de anidamiento, el frontend debe escribir res.data.data
Data + metadatos La mejor observabilidad Requiere un acuerdo estricto sobre qué campos pertenecen a meta

Recomendación: usa un envoltorio data de forma uniforme tanto para listas como para detalles, y nunca hagas que el nivel superior sea un array. Así, añadir paginación o IDs de trazabilidad después nunca rompe el contrato.

2. Formato de error: copia RFC 9457

Inventar tu propia estructura de error es una fuente enorme de coste de colaboración. RFC 9457 (que sustituye a RFC 7807) define application/problem+json:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Campo Significado Obligatorio
type URI estable del tipo de problema Recomendado
title Resumen corto legible por humanos Recomendado
status Código de estado HTTP Recomendado
detail Explicación concreta de lo que ocurrió esta vez Opcional
instance La instancia del recurso que tuvo el problema Opcional
Campos de extensión Campos de negocio arbitrarios Opcional

Los campos de extensión (como balance / required arriba) están permitidos por la especificación, y el frontend puede usarlos para mensajes precisos en lugar de analizar el texto de detail.

3. Paginación: cursor gana a offset

// Paginación por offset
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Paginación por cursor
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Dimensión Paginación por offset Paginación por cursor
Dificultad de implementación Baja (LIMIT/OFFSET) Media (requiere una clave de ordenación estable)
Rendimiento en páginas profundas Malo (cuanto mayor el offset, más lento) Bueno
Cuando cambian los datos Duplicados o elementos que faltan Estable
Salto a una página Posible No posible
Necesita un recuento total Requiere un count extra No requerido

Recomendación: usa paginación por offset para tablas de administración (necesitan saltos de página y totales), y paginación por cursor para feeds infinitos orientados al usuario.

4. IDs y tiempo

4.1 Usa siempre cadenas para los IDs

El razonamiento se cubrió en el artículo sobre tipos: en el lado JavaScript, los valores por encima de 2^53 pierden precisión silenciosamente. Incluso si el ID actual es un entero autoincremental pequeño, usar cadenas deja margen para cambiar después a IDs snowflake.

{ "id": "1000000000000000001" }

4.2 Usa cadenas RFC 3339 para el tiempo

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • No uses números de timestamp en milisegundos: son difíciles de leer, y la unidad (segundos/milisegundos) se confunde con frecuencia;
  • Especifica la zona horaria explícitamente: con Z o con un offset; nunca uses "hora local sin zona horaria";
  • Nombra los campos de forma uniforme como xxxAt para que se vea a simple vista que es un punto en el tiempo.

5. null y ausente

Debes elegir una opción y escribirla en la documentación:

Enfoque Expresar "sin valor" Expresar "no modificar"
Enfoque A El valor del campo es null Omitir el campo en la petición
Enfoque B El campo no existe Omitir el campo en la petición

El problema del Enfoque A es que "omitido" y "enviado como null" significan cosas distintas en un PATCH, lo que se malinterpreta fácilmente; el problema del Enfoque B es que el JSON.stringify del frontend descarta los campos cuyo valor es undefined, lo que resulta coincidir con la semántica.

Recomendación práctica:

  • Usa el Enfoque A para cuerpos de respuesta (null significa claramente sin valor, y el frontend no necesita distinguir undefined);
  • Usa el Enfoque B para cuerpos de petición (especialmente PATCH), combinado con Merge Patch o JSON Patch para hacer explícita la semántica.

6. Nomenclatura de campos y estructura

Decisión Opciones comunes Advertencia
Estilo de nomenclatura camelCase (ecosistema JS) / snake_case (Python, bases de datos) Uniforme en todo el proyecto, no mezclar
Valores de enum Minúsculas con guiones bajos, "pending_review" No usar números; son difíciles de leer
Campos booleanos isActive / enabled No usar status: 0/1
Campos de array Sustantivos en plural, items Devolver [] para arrays vacíos, nunca null
Profundidad de anidamiento No más de 3 niveles Más profundo que eso, divídelo o ofrece un parámetro expand

Devolver [] en lugar de null para arrays vacíos importa mucho: data.items.map(...) en el frontend no fallará por un null.

7. Estrategia de evolución

Cambio Rompe la compatibilidad Cómo gestionarlo
Añadir un campo opcional No Publicarlo directamente
Añadir un campo obligatorio Sí Añadir un valor por defecto, o publicar por versión
Eliminar un campo Sí Marcarlo primero como obsoleto, observar a los consumidores y luego eliminarlo
Cambiar el tipo de un campo Sí Añadir un nombre de campo nuevo y escritura dual durante la transición
Cambiar el conjunto de valores de enum Depende El cliente debe tener un fallback para valores desconocidos
Cambiar la semántica de un campo Sí Tratarlo como un campo nuevo

Tres reglas de disciplina:

  1. Los clientes deben ser tolerantes con campos desconocidos (no te encierres con additionalProperties: false);
  2. Los clientes deben tener una rama de fallback para valores de enum desconocidos;
  3. Mantén los campos obsoletos al menos una versión mayor, y márcalos con Deprecation en las cabeceras de respuesta o en la documentación.

8. Un ejemplo completo

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Esta respuesta refleja todas las convenciones anteriores: envoltorio data, IDs en cadena, importes como cadenas, marcas de tiempo con zona horaria, null significando ausente, array vacío, paginación por cursor, y meta para información de observabilidad.

9. Lista de comprobación

  • [ ] El nivel superior no es un array; usa un envoltorio data de forma uniforme;
  • [ ] Las respuestas de error siguen RFC 9457 e incluyen un type estable;
  • [ ] La forma de la paginación se ajusta al escenario de negocio;
  • [ ] Los IDs son cadenas, y el tiempo usa RFC 3339;
  • [ ] La semántica de null y ausente está escrita en la documentación;
  • [ ] El estilo de nomenclatura es uniforme en todo el proyecto, y los enums usan cadenas en minúsculas;
  • [ ] El cliente tiene fallbacks para campos desconocidos y valores de enum desconocidos;
  • [ ] Los cambios incompatibles pasan por versionado o por una transición de escritura dual.

FAQ

¿Cómo debo diseñar una API JSON evolucionable?

Envuelve listas y detalles de forma uniforme en un envoltorio data y nunca hagas que el nivel superior sea un array, para que la paginación y los IDs de trazabilidad puedan añadirse sin romper el contrato. Usa IDs en cadena y marcas de tiempo RFC 3339, y escribe en la documentación la semántica de null frente a ausente. Mantén los campos nuevos como opcionales y semánticamente independientes, y da a los clientes fallbacks para campos y valores de enum desconocidos.

¿Por qué no inventar un formato de error personalizado?

Inventar tu propia estructura de error es una fuente enorme de coste de colaboración. Copia application/problem+json de RFC 9457, identifica los problemas con un type estable y permite campos de extensión como balance y required, para que el frontend pueda construir mensajes precisos sin analizar el texto de detail.

¿Todos los endpoints deben usar paginación por cursor?

No. Las tablas de administración necesitan saltos de página y totales, así que la paginación por offset encaja mejor; los feeds infinitos orientados al usuario deben usar paginación por cursor, que se mantiene estable en páginas profundas y evita duplicados o huecos cuando cambian los datos. Elige según si el escenario necesita saltos de página y totales.