Gerenciando JSON para configuração, fixtures e dados locais
Resumo: A configuração precisa ser em camadas e validada; as fixtures precisam ser determinísticas, legíveis e comparáveis por diff. Se esses dois tipos de arquivo forem mal administrados, o custo de depuração fica alto por muito tempo.

Resumo da ópera: construa a configuração como padrões, ambiente e sobrescritas locais, com validação na inicialização antes de se preocupar com o formato do arquivo; torne as fixtures determinísticas em tempo, IDs, ordem de chaves e indentação para que permaneçam comparáveis por diff, e só então automatize o mascaramento.
Esses dois tipos de JSON não participam da lógica de negócio, mas determinam "quanto tempo leva para um novo colega começar a rodar" e "quanto tempo leva para localizar um problema de produção".
1. Configuração: uma estrutura em três camadas
| Camada | Origem | Mantido por | Commitado |
|---|---|---|---|
| Padrões | Constantes no código / default.json |
Desenvolvedores | Sim |
| Configuração de ambiente | config/production.json, variáveis de ambiente |
Operações | Sim (sem segredos) |
| Sobrescritas locais | .env.local, config/local.json |
Indivíduos | Não (ignorado pelo git) |
Ordem de merge: padrões → ambiente → local, com cada camada posterior sobrescrevendo a anterior. Mescle apenas tão profundamente quanto o negócio precisar; não faça merge profundo cegamente — o merge profundo torna problemas como "arrays foram concatenados inesperadamente" extremamente difíceis de enxergar.
/**
* Mescla camadas de configuração por prioridade; camadas posteriores sobrescrevem as anteriores, e arrays são substituídos por inteiro em vez de concatenados.
* @param layers Camadas de configuração da menor para a maior prioridade
* @returns O objeto de configuração mesclado
*/
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;
}
// Arrays e escalares são sempre substituídos por inteiro para evitar concatenação acidental de elementos
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Valide na inicialização, não espere até o tempo de execução
O custo de um erro de configuração cresce exponencialmente com o quão tarde ele é descoberto:
| Quando é descoberto | Custo |
|---|---|
| Na inicialização | Uma linha de log, reinicie e siga em frente |
| Na primeira requisição | Alguns recursos dão erro, uma hora de depuração |
| Quando um job agendado dispara | Alertas no meio da noite |
| No dia de fechamento | Um incidente de conciliação |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Valida a configuração e lança imediatamente em caso de falha, implementando fail fast.
* @param rawConfig A configuração mesclada crua
* @returns A configuração 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>;
}
Princípios de apoio:
- Saia imediatamente quando um item obrigatório estiver ausente; não forneça um padrão "que parece plausível" (como padronizar o endereço do banco de dados para
localhost); - Saia imediatamente em erros de tipo; a string
"false"não é o booleanofalse, então variáveis de ambiente precisam ser convertidas explicitamente após a leitura; - Segredos nunca entram no repositório; leia-os apenas de variáveis de ambiente ou de um serviço de gerenciamento de segredos.
3. Que formato usar para arquivos de configuração
| Arquivo | Formato | Motivo |
|---|---|---|
package.json |
JSON padrão | O gerenciador de pacotes só aceita isso |
tsconfig.json |
JSONC | Comentários permitidos, oficialmente suportado |
.vscode/settings.json |
JSONC | O mesmo que acima |
| Configuração de negócio | YAML ou JSONC | Precisa de comentários explicando o que cada item significa |
| Lista de variáveis de ambiente | .env |
Pares chave-valor planos, fáceis de injetar |
Um teste prático: este arquivo será lido por ferramentas não humanas? Se sim → JSON padrão; se só o seu próprio código o lê → um dialeto serve.
4. Fixtures de teste: diretórios e nomeação
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
Uma boa convenção de nomeação é objeto-cenário: order-missing-field é bem mais útil que test1.json.
Nomes de arquivo não-ASCII ocasionalmente causam problemas de codificação em cadeias de ferramentas multiplataforma, então mantenha os nomes de arquivo em inglês enquanto o conteúdo pode permanecer em chinês.
4.1 Três tipos de caso, três tipos de expectativa
| Diretório | Expectativa | Finalidade |
|---|---|---|
valid/ |
A validação passa | Fluxo principal, geração de tipos |
invalid/ |
A validação falha, e o erro aponta para o campo esperado | Tratamento de erro, texto de mensagem |
edge/ |
O comportamento é explícito (passar ou falhar tudo bem, mas precisa ser asseverado) | Proteção contra regressões |
O JSONTestSuite usa os prefixos y_ / n_ / i_ para os mesmos três tipos (deve aceitar / deve rejeitar / definido pela implementação),
e você pode tomar emprestada essa convenção de nomeação diretamente.
5. Determinismo: torne as fixtures comparáveis por diff
Fixtures instáveis tornam a revisão de código inútil:
| Fonte de ruído | Como lidar |
|---|---|
| Hora atual | Fixe-a em uma constante, por exemplo 2026-01-01T00:00:00Z |
| IDs aleatórios / UUIDs | Use uma sequência incremental ou valores fixos |
| Ordem das chaves | Ordene alfabeticamente no momento da geração |
| Formatação de float | Mantenha uniformemente duas casas decimais ou converta para strings |
| Fuso horário | Use UTC em toda parte |
| Indentação | Fixa em 2 espaços, verificada pelo Prettier no CI |
/**
* Ordena chaves recursivamente para produzir texto JSON estável e comparável por diff.
* @param value O valor a serializar
* @param indentWidth Largura da indentação
* @returns Uma string JSON com ordem estável
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Ordena recursivamente as chaves de objeto.
* @param input Qualquer valor JSON
* @returns Um novo valor com chaves 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. Mascaramento e conformidade
Dados reais não podem ser usados diretamente como fixtures:
- Nomes → placeholders como "John Doe" e "Jane Roe";
- Números de telefone → números obviamente fictícios como
13800138000; - Documentos de identidade → números falsos com o formato correto mas com dígito verificador inválido;
- Endereços → mantenha a estrutura de província/cidade, descarte o número do imóvel;
- Valores → mantenha a ordem de magnitude, mas nunca mantenha informações reais de conta.
É melhor que a equipe tenha um scripts/mask-fixture.mjs que rode sobre uma amostra de produção exportada para gerar uma versão mascarada,
evitando "mudar três lugares à mão e esquecer o resto".
7. Migração e versões
Fixtures também envelhecem. Adicione um campo schemaVersion à fixture, ou coloque um arquivo VERSION no diretório:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Quando o contrato de API é atualizado:
- Adicione um novo diretório de fixtures (
v3/); não modifique fixtures antigas no lugar; - Mantenha as fixtures antigas para regressão até que a versão antiga da API seja aposentada;
- Rode as duas versões no CI para confirmar a compatibilidade retroativa.
8. Lista de verificação
- [ ] A estrutura de configuração em três camadas está clara, e sobrescritas locais não são commitadas;
- [ ] A validação acontece na inicialização, e a falha sai imediatamente;
- [ ] Segredos vêm apenas de variáveis de ambiente; arquivos de configuração contêm apenas placeholders;
- [ ] As fixtures são divididas em diretórios
valid/invalid/edge, e os nomes expressam o cenário; - [ ] As fixtures são determinísticas (tempo, IDs e ordem fixos) e comparáveis por diff;
- [ ] Dados reais foram processados pelo script de mascaramento;
- [ ] Atualizações de contrato adicionam um novo diretório de versão em vez de editar no lugar.
Perguntas frequentes
Como as camadas de configuração devem ser ordenadas e mescladas?
Use três camadas — padrões, configuração de ambiente e sobrescritas locais — mesclando padrões → ambiente → local, com cada camada posterior sobrescrevendo a anterior. Mescle apenas tão profundamente quanto o negócio precisa e substitua arrays e escalares por inteiro, para que o merge profundo não concatene arrays inesperadamente. Ignore no git o arquivo de sobrescrita local em vez de commitar configurações pessoais.
Por que validar a configuração na inicialização?
O custo de um erro de configuração cresce exponencialmente com o quão tarde ele é descoberto: uma linha de log na inicialização versus um incidente de conciliação no dia de fechamento. Valide contra um JSON Schema na inicialização e saia imediatamente em itens obrigatórios ausentes ou erros de tipo, em vez de fornecer um padrão "que parece plausível".
Devo usar dados de produção diretamente como fixtures de teste?
Não. Nomes, números de telefone, documentos de identidade, endereços e valores todos precisam de placeholders ou valores obviamente fictícios, e a equipe deve ter um scripts/mask-fixture.mjs que rode sobre uma exportação de produção para gerar uma versão mascarada, evitando edições manuais que esqueçam algo.