JSON Patch em produção: atomicidade, idempotência e conflitos concorrentes
Resumo: JSON Patch é apenas um formato de mudança; atomicidade, idempotência e concorrência continuam sendo seu trabalho. Cobre pré-condições `test`, escape de caminho, rollback e coordenação de ETag.

Resumo da ópera: valide os caminhos alvo e permissões do patch primeiro e depois adicione uma asserção de concorrência com test ou If-Match; aplique o patch a uma cópia para que uma falha não deixe nada meio-escrito.
O perigo real de uma requisição
PATCHnão é gravar o campo errado. É acreditar que a gravação foi bem-sucedida enquanto sobrescreve silenciosamente a mudança de outra pessoa. O JSON Patch lhe dá as ferramentas, mas o projeto de segurança ainda é seu.
1. Uma revisão rápida do conjunto de operações da RFC 6902
O JSON Patch descreve mudanças como um array de operações, com o tipo de mídia application/json-patch+json. Seis operações são executadas na ordem do array:
| Operação | Efeito | Restrição-chave |
|---|---|---|
add |
Adicionar um membro ou inserir um elemento de array | Um índice de array pode ser igual ao comprimento; - significa o fim |
remove |
Remover um membro ou elemento | O caminho alvo precisa existir |
replace |
Substituir um valor existente | O caminho alvo precisa existir |
move |
Mover um nó | Não pode mover um nó para dentro do seu próprio caminho filho |
copy |
Copiar um nó | O caminho de origem precisa existir |
test |
Assegurar igualdade de valor | Se falhar, o patch inteiro não é aplicado |
Muitas pessoas só lembram das três primeiras e esquecem que test é o que transforma uma escrita cega em uma escrita condicional. A semântica completa das operações e as fronteiras de array são cobertas em JSON Pointer, Patch e Merge Patch.
2. Usando test para transformar um patch em uma atualização condicional
Um patch típico de bloqueio otimista não começa com uma escrita, mas com uma asserção de versão:
{"op":"test","path":"/version","value":1}
Coloque-o em um patch completo:
[
{ "op": "test", "path": "/version", "value": 1 },
{ "op": "replace", "path": "/version", "value": 2 },
{ "op": "replace", "path": "/status", "value": "paid" }
]
A lógica é: confirme que a versão do documento atualmente é 1 e depois mude-a para 2. Se outra requisição já avançou a versão para 2, o primeiro test falha, o patch inteiro não é aplicado e o servidor retorna um conflito. Isso previne atualizações perdidas sem introduzir um bloqueio distribuído.
Alguns pontos de design:
- Coloque
testantes de cada escrita. Se uma escrita anterior já entrou em vigor, a asserção é sem sentido; - Assegure um número de versão ou digest gerado pelo servidor, não um campo comum que o cliente pode mudar livremente;
- Incremente a versão a cada escrita bem-sucedida; caso contrário, duas requisições usando a mesma versão antiga passam;
- O
valuedetestprecisa ter exatamente o mesmo tipo JSON que o valor do documento.1e"1"não são iguais.
3. Escape de caminho e prototype pollution
As regras de escape do JSON Pointer são simples: escreva ~ como ~0 e / como ~1, e substitua ~ antes de /, caso contrário você obtém escape duplo.
Chave "a/b" → /a~1b
Chave "m~n" → /m~0n
Chave "a~/b" → /a~0~1b
O risco com patches externos normalmente não é o escape, mas chaves perigosas. Em JavaScript, chaves como __proto__, constructor e prototype podem acionar prototype pollution, até por um add comum:
[
{ "op": "add", "path": "/__proto__/polluted", "value": true }
]
Use uma allowlist em vez de uma denylist:
- Permita que patches modifiquem apenas campos de negócio, como
/statusou/items/0/quantity; - Valide cada
pathefromcontra uma expressão regular ou prefixo de caminho, rejeitando níveis inesperados além de índices de array; - Analise JSON em contêineres
Object.create(null)ouMappara reduzir o impacto de uma cadeia de protótipos poluída; - Limite o número de operações e o tamanho em bytes de cada
value.
4. Aplicação atômica e rollback
A RFC 6902 exige execução atômica, mas atomicidade é um requisito semântico, não um padrão em toda biblioteca. Bibliotecas como fast-json-patch mutam o documento no lugar quando mutateDocument é true, e uma falha pode deixar mudanças parciais para trás.
import { applyPatch, validate } from 'fast-json-patch';
/**
* Aplica um patch imutavelmente e retorna o documento original em caso de falha.
* @param document O documento original, que os chamadores não devem mutar
* @param patch O array de operações JSON Patch a aplicar
* @returns O resultado, incluindo uma flag de sucesso e o novo documento
*/
function applySafely(document: unknown, patch: unknown[]) {
const errors = validate(patch, document);
if (errors) {
return { ok: false, document };
}
try {
const result = applyPatch(document, patch, false, false);
return { ok: true, document: result.newDocument };
} catch {
return { ok: false, document };
}
}
Três recomendações de implementação:
- Faça cópia profunda ou clonagem estruturada primeiro, aplique na cópia e só troque a referência após o sucesso;
- Não confie no rollback automático de uma biblioteca; o comportamento em exceções difere entre implementações;
- Valide antes de executar:
validateverifica apenas a estrutura, enquanto conflitos semânticos ainda exigemtest; nenhum substitui o outro.
Se um patch abrange múltiplos recursos, como mudar um pedido e seu inventário juntos, a atomicidade do JSON Patch vale apenas dentro de um único documento. Entre documentos você precisa de uma transação de banco de dados ou de ações compensatórias.
5. Idempotência: PATCH não é naturalmente idempotente
A semântica HTTP define PUT e DELETE como idempotentes, mas PATCH não é necessariamente. Repetir o mesmo JSON Patch pode produzir resultados completamente diferentes:
add /items/-acrescenta ao fim do array toda vez, então retentativas criam duplicatas;replace /versionmuda a versão 1 para 2 e tem sucesso de novo em uma retentativa, mesmo que a operação já tenha sido aplicada;remove /tempfalha na segunda execução porque o caminho não existe mais.
Há três formas comuns de obter idempotência:
| Abordagem | Como funciona | Melhor para |
|---|---|---|
| Pré-condição de versão | A primeira operação é test /version, depois incrementa |
Recursos com uma versão explícita |
| Chave de idempotência | O cliente envia Idempotency-Key; o servidor faz cache do resultado |
Pagamentos, pedidos e outras escritas |
| Patch de estado alvo | Use replace em vez de add, para que o resultado dependa apenas do valor final |
Configuração e campos de status |
O pior padrão é um patch que mistura add e remove presumindo que retentativas são seguras. Se um patch acrescenta ou exclui qualquer coisa, ele precisa estar pareado com uma asserção de versão ou uma chave de idempotência.
6. Conflitos concorrentes: ETag e If-Match
Além do test dentro do patch, o HTTP oferece um mecanismo padrão: o servidor retorna um ETag, e o cliente envia If-Match na próxima requisição.
GET /orders/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
ETag: "a3f9c1"
Content-Type: application/json
PATCH /orders/42 HTTP/1.1
Host: api.example.com
If-Match: "a3f9c1"
Content-Type: application/json-patch+json
[{"op":"replace","path":"/status","value":"paid"}]
Se o recurso mudou, o servidor retorna 412 Precondition Failed, e o cliente busca de novo antes de retentar. If-Match: * significa "execute desde que o recurso exista", o que não é adequado quando você precisa prevenir sobrescritas.
A diferença entre test e If-Match:
If-Matché verificado na camada HTTP antes da análise do corpo, então é mais barato;testé verificado na camada semântica do patch e pode assegurar qualquer campo sem depender de suporte a ETag no servidor;- Eles podem ser combinados, mas não implemente apenas um deles e afirme que o endpoint é seguro sob concorrência.
7. Diferenças entre bibliotecas e seleção
| Implementação | Linguagem | Atomicidade | Suporte a test |
Observações |
|---|---|---|---|---|
fast-json-patch |
JavaScript | Opcional (retorna um novo documento por padrão) | Sim | Ecossistema amplo; cuidado com mutateDocument |
jsonpatch |
Python | Reverte em exceções | Sim | Próxima do padrão, com erros claros |
json-patch |
Go | Opera em uma cópia | Sim | Frequentemente usada junto com JSON Merge Patch |
zjsonpatch |
Java | Sim | Sim | Integração conveniente com Jackson |
Não selecione uma biblioteca apenas por "suporta as seis operações". Confirme três coisas: se uma falha reverte, se test compara estritamente e se um add de array fora da faixa gera erro. Implementações frouxas fazem o mesmo patch se comportar de forma diferente entre ambientes.
8. Uma lista de verificação de segurança antes do lançamento
- [ ] O endpoint declara
application/json-patch+json; - [ ] Patches são aplicados a uma cópia, e uma falha nunca contamina o documento original;
- [ ] Todo
pathefromem patches externos passa por uma checagem de allowlist; - [ ] Chaves perigosas como
__proto__,constructoreprototypesão rejeitadas; - [ ] Contagem de operações e tamanho de valor único têm limites;
- [ ]
testouIf-Matchfornece controle de concorrência; - [ ] Patches são gravados em um log de auditoria com ator, timestamp e o array completo de operações;
- [ ] As fronteiras de transação são explícitas quando um patch abrange documentos.
Um patch é, por natureza, uma descrição completa de "o que mudou", o que o torna um registro de auditoria natural. Para códigos de erro e idempotência na camada de API, continue em Projeto de API JSON. Se um patch vem da mesclagem de duas branches, você também precisa de semântica de conflito, coberta em Merge do Git para arquivos JSON.
Perguntas frequentes
Que código de status uma operação test que falhou deve retornar?
Se o conflito vem de uma asserção de versão, tanto 409 Conflict quanto 412 Precondition Failed são defensáveis. Prefira 412 para manter consistência com If-Match, e inclua a versão atual no corpo da resposta para que o cliente consiga retentar.
Um patch contendo add e remove pode ser retentado com segurança?
Não diretamente. add no fim de um array duplica dados, e remove falha na segunda execução porque o caminho sumiu. Adicione uma pré-condição de versão ou uma chave de idempotência para que retentativas sejam reconhecidas como a mesma operação.
JSON Patch consegue garantir transações entre documentos?
Não. A atomicidade da RFC 6902 vale para um único documento JSON. Quando pedidos, inventário ou outros recursos estão envolvidos, use transações de banco de dados, compensação Saga ou uma fila de mensagens para consistência.
Por que um patch nunca deve gravar em __proto__?
Porque a maioria das implementações JavaScript armazena o documento em objetos comuns, e gravar __proto__ pode alterar a cadeia de protótipos e afetar todo objeto depois. Uma allowlist de caminhos é a defesa mais confiável; não confie apenas na filtragem interna de uma biblioteca.
Como torno uma requisição PATCH idempotente?
Há três abordagens comuns: comece o patch com test /version e incremente a versão em caso de sucesso, faça o servidor cachear o resultado para uma Idempotency-Key, ou use replace em vez de add para que o resultado dependa apenas do valor final. Se um patch acrescenta ou exclui qualquer coisa, ele precisa estar pareado com uma asserção de versão ou uma chave de idempotência; ele não pode simplesmente ser retentado.
Devo usar test e If-Match juntos?
Você não precisa usar os dois, mas implementar apenas um deles não torna um endpoint seguro sob concorrência. If-Match é verificado na camada HTTP antes da análise do corpo, então é mais barato; test é verificado na camada semântica do patch e pode assegurar qualquer campo sem depender de suporte a ETag no servidor.