Gestión de JSON para configuración, fixtures y datos locales
En resumen: La configuración debe estar por capas y validada; los fixtures deben ser deterministas, legibles y comparables con diff. Si estos dos tipos de archivos se gestionan mal, el coste de diagnóstico se mantiene alto durante mucho tiempo.

Conclusión: construye la configuración como valores por defecto, entorno y overrides locales, con validación al arranque, antes de preocuparte por el formato de archivo; haz que los fixtures sean deterministas en tiempo, IDs, orden de claves y sangría para que sigan siendo comparables con diff, y solo entonces automatiza el enmascarado.
Estos dos tipos de JSON no participan en la lógica de negocio, y aun así determinan "cuánto tarda un compañero nuevo en ponerse en marcha" y "cuánto tarda localizar un problema en producción".
1. Configuración: una estructura de tres capas
| Capa | Origen | Mantenida por | Versionada |
|---|---|---|---|
| Valores por defecto | Constantes en el código / default.json |
Desarrolladores | Sí |
| Configuración de entorno | config/production.json, variables de entorno |
Operaciones | Sí (sin secretos) |
| Overrides locales | .env.local, config/local.json |
Individuos | No (ignorado por git) |
Orden de fusión: valores por defecto → entorno → local, con cada capa posterior sobrescribiendo la anterior. Fusiona solo tan profundo como lo necesite el negocio; no hagas fusiones profundas a ciegas: la fusión profunda hace que problemas como "los arrays se concatenaron inesperadamente" sean extremadamente difíciles de detectar.
/**
* Fusiona capas de configuración por prioridad; las capas posteriores sobrescriben a las anteriores, y los arrays se reemplazan por completo en lugar de concatenarse.
* @param layers Capas de configuración de menor a mayor prioridad
* @returns El objeto de configuración fusionado
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// Los arrays y los escalares siempre se reemplazan por completo para evitar concatenaciones accidentales de elementos
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Valida al arranque, no esperes al tiempo de ejecución
El coste de un error de configuración crece exponencialmente con lo tarde que se descubre:
| Cuándo se descubre | Coste |
|---|---|
| Al arranque | Una línea de log, reinicias y sigues |
| En la primera petición | Algunas funcionalidades fallan, una hora de depuración |
| Cuando se dispara un trabajo programado | Alertas en mitad de la noche |
| El día de la liquidación | Un incidente de conciliación |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Valida la configuración y lanza de inmediato si falla, implementando fail fast.
* @param rawConfig La configuración fusionada en bruto
* @returns La configuración validada
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`配置校验失败:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
Principios de apoyo:
- Sal inmediatamente cuando falte un elemento obligatorio; no proporciones un valor por defecto "que parece plausible" (como poner
localhostcomo dirección de base de datos por defecto); - Sal inmediatamente ante errores de tipo; la cadena
"false"no es el booleanofalse, así que las variables de entorno deben convertirse explícitamente después de leerse; - Los secretos nunca entran en el repositorio; léelos solo desde variables de entorno o desde un servicio de gestión de secretos.
3. Qué formato usar para los archivos de configuración
| Archivo | Formato | Motivo |
|---|---|---|
package.json |
JSON estándar | El gestor de paquetes solo acepta esto |
tsconfig.json |
JSONC | Comentarios permitidos, soportado oficialmente |
.vscode/settings.json |
JSONC | Igual que el anterior |
| Configuración de negocio | YAML o JSONC | Necesita comentarios que expliquen qué significa cada elemento |
| Lista de variables de entorno | .env |
Pares clave-valor planos, fáciles de inyectar |
Una prueba práctica: ¿este archivo lo leerá alguna herramienta que no sea humana? Si sí → JSON estándar; si solo lo lee tu propio código → un dialecto vale.
4. Fixtures de prueba: directorios y nomenclatura
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
Una buena convención de nomenclatura es objeto-escenario: order-missing-field es mucho más útil que test1.json.
Los nombres de archivo no ASCII a veces causan problemas de codificación en toolchains multiplataforma, así que mantén los nombres de archivo en inglés mientras el contenido puede seguir en chino.
4.1 Tres tipos de casos, tres tipos de expectativas
| Directorio | Expectativa | Propósito |
|---|---|---|
valid/ |
La validación pasa | Flujo principal, generación de tipos |
invalid/ |
La validación falla, y el error apunta al campo esperado | Manejo de errores, redacción de mensajes |
edge/ |
El comportamiento está explícito (pasar o fallar da igual, pero debe verificarse) | Protección contra regresiones |
JSONTestSuite usa los prefijos y_ / n_ / i_ para los mismos tres tipos (debería aceptar / debería rechazar / definido por la implementación),
y puedes tomar prestada esa convención de nomenclatura directamente.
5. Determinismo: haz que los fixtures sean comparables con diff
Los fixtures inestables inutilizan la revisión de código:
| Fuente de ruido | Cómo gestionarla |
|---|---|
| Hora actual | Fíjala a una constante, p. ej. 2026-01-01T00:00:00Z |
| IDs aleatorios / UUIDs | Usa una secuencia incremental o valores fijos |
| Orden de claves | Ordena alfabéticamente en el momento de la generación |
| Formato de decimales | Mantén uniformemente dos decimales o convierte a cadenas |
| Zona horaria | Usa UTC en todas partes |
| Sangría | Fija en 2 espacios, comprobada por Prettier en CI |
/**
* Ordena las claves de forma recursiva para producir texto JSON estable y comparable con diff.
* @param value El valor que se va a serializar
* @param indentWidth Ancho de la sangría
* @returns Una cadena JSON con orden estable
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Ordena las claves de los objetos de forma recursiva.
* @param input Cualquier valor JSON
* @returns Un valor nuevo con las claves ordenadas
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
6. Enmascarado y cumplimiento normativo
Los datos reales no pueden usarse directamente como fixtures:
- Nombres → marcadores de posición como "John Doe" y "Jane Roe";
- Números de teléfono → números obviamente ficticios como
13800138000; - Números de documento de identidad → números falsos con el formato correcto pero un dígito de control inválido;
- Direcciones → mantener la estructura provincia/ciudad, quitar el número de la casa;
- Importes → mantener el orden de magnitud, pero nunca mantener información real de cuentas.
Lo mejor es que el equipo tenga un scripts/mask-fixture.mjs que se ejecute sobre una muestra de producción exportada para generar una versión enmascarada,
evitando "cambiar tres sitios a mano y olvidar el resto".
7. Migración y versiones
Los fixtures también se quedan obsoletos. Añade un campo schemaVersion al fixture, o coloca un archivo VERSION en el directorio:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Cuando se actualiza el contrato de API:
- Añade un directorio de fixtures nuevo (
v3/); no modifiques los fixtures antiguos in situ; - Mantén los fixtures antiguos para regresión hasta que se retrase la versión antigua de la API;
- Ejecuta ambas versiones en CI para confirmar la compatibilidad hacia atrás.
8. Lista de comprobación
- [ ] La estructura de configuración en tres capas es clara, y los overrides locales no se versionan;
- [ ] La validación ocurre al arranque, y el fallo termina de inmediato;
- [ ] Los secretos solo vienen de variables de entorno; los archivos de configuración contienen únicamente marcadores de posición;
- [ ] Los fixtures están divididos en directorios
valid/invalid/edge, y los nombres expresan el escenario; - [ ] Los fixtures son deterministas (tiempo, IDs y orden fijos) y comparables con diff;
- [ ] Los datos reales han pasado por el script de enmascarado;
- [ ] Las actualizaciones de contrato añaden un directorio de versión nuevo en lugar de editar in situ.
FAQ
¿Cómo deben ordenarse y fusionarse las capas de configuración?
Usa tres capas — valores por defecto, configuración de entorno y overrides locales — fusionando valores por defecto → entorno → local, con cada capa posterior sobrescribiendo a la anterior. Fusiona solo tan profundo como lo necesite el negocio y reemplaza arrays y escalares por completo, para que la fusión profunda no pueda concatenar arrays inesperadamente. Ignora con git el archivo de override local en lugar de versionar ajustes personales.
¿Por qué validar la configuración al arranque?
El coste de un error de configuración crece exponencialmente con lo tarde que se descubre: una línea de log al arranque frente a un incidente de conciliación el día de la liquidación. Valida contra un JSON Schema al arranque y sal de inmediato ante elementos obligatorios ausentes o errores de tipo, en lugar de proporcionar un valor por defecto "que parece plausible".
¿Debo usar datos de producción directamente como fixtures de prueba?
No. Nombres, números de teléfono, números de documento de identidad, direcciones e importes necesitan todos marcadores de posición o valores obviamente ficticios, y el equipo debe tener un scripts/mask-fixture.mjs que se ejecute sobre una exportación de producción para generar una versión enmascarada, evitando ediciones manuales que se dejan algo fuera.