JSON Toolkit

JSON Pointer, Patch e Merge Patch

Resumo: Três especificações curtas formam um protocolo de mudança: Pointer localiza, Patch edita com precisão, Merge Patch mescla parcialmente. Escolher errado dói em arrays e concorrência.

Intermediário~12 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de JSON Pointer, Patch e Merge Patch

Resumo da ópera: use JSON Pointer para localizar por posição e JSONPath para filtrar ou descer recursivamente; entregue inserção/remoção em array, gravações de null e asserções de versão ao JSON Patch, e reserve o Merge Patch para configuração plana e recursos de formulário.

Localizar e modificar são duas coisas diferentes, e as especificações as dividem em três documentos. O preço usual de confundi-las é "o array inteiro foi substituído" e ninguém percebe.

1. JSON Pointer: a sintaxe de localização mais curta

A RFC 6901 faz exatamente uma coisa: apontar para um nó em um documento com uma string.

""             → o documento inteiro
"/store"       → o membro store sob a raiz
"/store/book"  → um nível mais fundo
"/store/book/0/title" → item 0 do array

Três regras:

  1. A string vazia aponta para o próprio nó raiz, não para um "nó vazio";
  2. Cada / separa um token de referência; arrays usam índices decimais (zeros à esquerda não são permitidos, então 01 é inválido);
  3. Dentro de um token, ~ é escrito como ~0 e / como ~1 — substitua ~ antes de /, caso contrário você acaba fazendo escape duplo.
/**
 * Escapa um único token de referência em um segmento de JSON Pointer.
 * @param token Nome de chave ou índice original
 * @returns Token escapado
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Junta um array de nomes de chave em um JSON Pointer.
 * @param tokens Array de nomes de chave ou índices
 * @returns String de pointer começando com /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

O valor aparece melhor quando a própria chave contém /: o pointer de {"a/b": 1} é /a~1b, não /a/b (o último significa b sob a).

Quando usado dentro de uma URL, a codificação percentual também é necessária, porque # e ~ têm significados especiais em um fragmento.

2. JSON Patch: uma sequência atômica de seis operações

A RFC 6902 descreve mudanças com um array de operações, tipo de mídia application/json-patch+json:

[
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/name", "value": "新的名字" },
  { "op": "add", "path": "/tags/-", "value": "json" },
  { "op": "remove", "path": "/temp" },
  { "op": "copy", "from": "/name", "path": "/alias" },
  { "op": "move", "from": "/tags/0", "path": "/tags/1" }
]
Operação Semântica Fronteira
add Adiciona um membro ou insere um elemento de array O índice de array pode ser igual ao comprimento (acrescentar); - significa o fim
remove Remove O caminho precisa existir
replace Substitui um valor existente O caminho precisa existir (equivalente a remove + add, mas exige que o caminho exista primeiro)
move Move Você não pode mover um nó para baixo do seu próprio subcaminho
copy Copia A origem precisa existir
test Assegura igualdade Se falhar, o patch inteiro não é aplicado; usado para bloqueio otimista

A propriedade-chave é a atomicidade: se qualquer operação falhar, o patch inteiro é revertido, então não existe estado intermediário em que "as três primeiras foram aplicadas e a quarta falhou". Isso o torna um ótimo corpo de requisição PATCH.

2.1 Usando test para concorrência otimista

[
  { "op": "test", "path": "/etag", "value": "a3f9c1" },
  { "op": "replace", "path": "/status", "value": "paid" }
]

O cliente primeiro faz GET do etag e o grava na operação test. Em escritas concorrentes, o test da segunda requisição falha e o servidor retorna 409 Conflict ou 412 Precondition Failed. Isso é bem mais seguro do que last-write-wins e não exige bloqueio distribuído.

2.2 Operações em array são as mais propensas a erro

// Quer inserir no índice 1
{ "op": "add", "path": "/items/1", "value": {...} }   // Correto: insere; o índice 1 original e tudo depois desliza para trás

// Quer substituir o índice 1
{ "op": "replace", "path": "/items/1", "value": {...} } // Correto: substitui

add no índice de array n é uma inserção, não uma sobrescrita; um índice fora da faixa (n > length) precisa gerar erro. Algumas implementações são lenientes quanto a isso, o que faz o mesmo patch se comportar de forma diferente entre servidores.

3. Merge Patch: um fragmento que parece o alvo

A RFC 7386 tem apenas quatro regras:

  1. O alvo é um objeto e o patch é um objeto → mesclar recursivamente;
  2. O valor de uma chave no patch é null → excluir essa chave do alvo;
  3. O valor de uma chave no patch não é um objeto → substituir diretamente;
  4. O patch em si não é um objeto → o alvo inteiro é substituído.
// Alvo
{ "a": "b", "c": { "d": "e", "f": "g" } }

// Patch
{ "a": "z", "c": { "f": null } }

// Resultado
{ "a": "z", "c": { "d": "e" } }

O tipo de mídia é application/merge-patch+json. Ele se lê como um "update parcial", o que o torna especialmente fácil de usar indevidamente:

  • Arrays só podem ser substituídos por inteiro. Quer excluir list[2]? O Merge Patch não consegue;
  • Você não consegue gravar null. null já está tomado para significar exclusão;
  • Ele não consegue expressar um terceiro estado entre "deixe este campo intacto" e "exclua este campo" (por exemplo, definir explicitamente como vazio).

4. Matriz de capacidades: qual escolher

Requisito JSON Patch Merge Patch
Substituir precisamente um elemento específico de array ✓ ✗
Excluir um item de um array ✓ ✗
Atualizar parcialmente vários campos ✓ (vários replace) ✓ (o mais conciso)
Gravar um valor null ✓ ✗
Controle de concorrência (asserção de versão) ✓ (test) ✗ (apenas via If-Match)
Legibilidade do patch Moderada Boa (parece os próprios dados)
Complexidade de implementação Alta Baixa
Formas de recurso adequadas Profundamente aninhadas, tipo lista Configuração plana, formulários

Regras gerais:

  • Formulários, páginas de configuração, recursos de configuração → Merge Patch; o frontend só envia os campos alterados;
  • Adições e remoções de listas, transições de máquina de estados, qualquer coisa que precise de proteção de concorrência → JSON Patch;
  • Ambos podem coexistir: endpoints diferentes declaram Content-Types diferentes, e o servidor despacha por tipo.

5. Notas de implementação

import { applyPatch, compare } from 'fast-json-patch';

const document = { version: 7, items: [{ id: 1 }, { id: 2 }] };
const patch = compare(document, { version: 8, items: [{ id: 1 }] });

// operações test são validadas por padrão; definir mutateDocument como true modifica no lugar, use com cautela
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

Quatro pontos facilmente negligenciados:

  1. Não mute objetos compartilhados no lugar; um patch que falha deixa um estado meio-modificado (a menos que a implementação garanta rollback);
  2. Validação de caminho: patches vindos de fora precisam validar cada path/from e rejeitar chaves como __proto__ e constructor;
  3. Limites de tamanho: limite o número de entradas do patch para impedir que um único patch execute centenas de milhares de operações;
  4. Auditoria: patches são naturalmente adequados a logs de auditoria — são uma descrição completa de "o que mudou".

6. Semântica de erro

Situação Código de status sugerido Explicação
O patch não é um array / é estruturalmente inválido 400 Bad Request O formato do corpo da requisição está errado
O nó referenciado por path não existe 422 Unprocessable Entity Sintaticamente válido mas semanticamente não executável
Uma operação test falhou 409 ou 412 Indica um conflito de versão
Sem permissão para modificar aquele caminho 403 Forbidden Não mascare com 404 a menos que você oculte deliberadamente a existência do recurso

Distinguir 400 de 422 importa: o primeiro significa "não consigo entender seu patch", o segundo significa "eu entendo, mas estes dados não permitem essa mudança".

7. Lista de verificação

  • [ ] O endpoint declara o Content-Type correto (json-patch+json / merge-patch+json);
  • [ ] A aplicação do patch é atômica, e uma falha não deixa resultado parcial;
  • [ ] Patches externos são validados contra uma whitelist de caminhos e um limite de contagem de entradas;
  • [ ] O controle de concorrência usa test ou If-Match;
  • [ ] Patches são gravados no log de auditoria;
  • [ ] Cenários de array escolhem explicitamente JSON Patch; não conte com Merge Patch.

Perguntas frequentes

Como decido entre JSON Patch e Merge Patch?

Use JSON Patch quando precisar substituir ou remover um elemento específico de array, gravar um valor null ou assegurar uma versão; use Merge Patch para formulários e recursos de configuração que atualizam apenas alguns campos. Merge Patch não consegue excluir um único item de array, nem consegue gravar null, porque null já significa exclusão.

Por que add no índice de array n não é uma sobrescrita?

Porque add insere em um array: o elemento no índice n e tudo depois dele desliza para trás. Para substituir um elemento existente você precisa usar replace, e o caminho precisa existir primeiro. Um índice fora da faixa (n > length) deveria gerar erro; implementações lenientes fazem o mesmo patch se comportar de forma diferente entre servidores.

Devo modificar o objeto original no lugar?

Não. Um patch que falha deixa um estado meio-modificado a menos que a implementação garanta rollback. Aplique o patch a uma cópia profunda ou clonada estruturalmente e depois troque a referência após o sucesso.