Perfil de canonicalização do DCP-AI¶
Tradução informativa da especificação em inglês, atualizada em 25/10/2026. A versão normativa é a inglesa: esta tradução não tem valor normativo e, em caso de divergência, prevalece o inglês. Identificadores e blocos de código são mantidos sem tradução.
Identificador do perfil: dcp-jcs-v1
Estado: Publicado — v2.0
Incluído pela primeira vez em: Python 2.8.0, Rust 2.8.0, Go v2.8.0, TypeScript 2.1.0
Este documento é a referência normativa da canonicalização JSON
exata byte a byte no DCP-AI. Ele substitui o resumo de cinco tópicos que
aparecia em spec/DCP-AI-v2.0.md § 15. Cada assinatura, cada hash e
cada folha de Merkle de um artefato do DCP são calculados sobre a saída do
algoritmo especificado aqui.
O perfil é baseado na RFC 8785 (JCS). Ele restringe os números a inteiros (regra 4) e dá tratamento explícito aos casos que o JCS deixa definidos pela implementação. Não é um subconjunto estrito do JCS: a regra 1 ordena as chaves dos objetos por ponto de código Unicode, enquanto a RFC 8785 §3.2.3 as ordena por unidades de código UTF-16. As duas ordens só diferem quando as chaves de um objeto misturam caracteres do plano suplementar (U+10000 em diante) com caracteres de U+E000..U+FFFF. Os SDKs que afirmam cumprir o perfil devem (MUST) produzir uma saída idêntica byte a byte para a tabela de casos-limite do §3.
1. Por que um perfil e não simplesmente “JCS”¶
A RFC 8785 fixa as regras estruturais —chaves ordenadas, forma compacta, escape
de strings, representação de números— mas deixa algumas decisões à
linguagem hospedeira do produtor: como undefined é tratado, o que acontece com
floats que têm valor inteiro, se 1e2 é normalizado,
como os tipos do hospedeiro equivalentes a null são mapeados para JSON null. Em quatro SDKs
(TypeScript, Python, Rust, Go) esses padrões divergem e produzem
bytes diferentes para o mesmo objeto de origem. dcp-jcs-v1 fixa cada uma
dessas decisões para que os quatro SDKs produzam uma saída idêntica.
2. Regras normativas¶
Um produtor que implementa dcp-jcs-v1 deve (MUST) respeitar as regras 1 a 8.
Um verificador que consome dcp-jcs-v1 deveria (SHOULD) rejeitar artefatos cuja
saída se desvie destas regras.
Regra 1 — Chaves ordenadas¶
As chaves dos objetos são ordenadas lexicograficamente por ponto de código Unicode em ordem crescente. A ordenação é feita em todos os níveis de aninhamento.
Regra 2 — Forma compacta¶
Nenhum espaço em branco fora dos valores de string. O separador de pares é ,,
o separador entre chave e valor é :. Sem vírgulas finais.
Regra 3 — Escape de strings¶
As strings são codificadas em UTF-8. O escape segue a RFC 8785 §3.2: só são escapados os
caracteres que o JSON exige escapar (caracteres de controle U+0000
a U+001F, ", \). A barra, os caracteres
não ASCII, os emoji, os kanji etc. são emitidos literalmente como a sua sequência de bytes
UTF-8.
Regra 4 — Valores numéricos: apenas inteiros¶
O formato de transmissão só aceita números cujo valor é um inteiro exato. “Inteiro exato” é decidido depois da análise:
- Um
NumberJSON é válido se e somente se (a) for finito e (b) o seu valor não tiver parte fracionária (floor(n) == n). - Um
Numbercujo literal de origem era1.0,1.00,1e2,100ou-0é válido porque todos esses literais são decodificados para um valor inteiro após a análise. - Um
Numbercujo valor é0.1,1.5,1e-1,NaNou±Infinitynão é válido e o canonicalizador deve (MUST) lançar um erro antes de produzir qualquer saída.
Normalização da saída: todo número válido é emitido em forma de inteiro simples,
sem ponto decimal, sem expoente, sem zeros à esquerda (exceto
0) e sem zeros finais.
{"n": 1.0} → {"n":1}
{"n": 1e2} → {"n":100}
{"n": -0} → {"n":0}
{"n": 0.1} → ERROR
{"n": NaN} → ERROR
Esta regra é o que faz os quatro SDKs convergirem: um produtor JavaScript
pode passar 1.0 como literal (não tem tipo inteiro), um produtor Python
pode passar int(100), um produtor Go pode passar 1e2 analisado a partir de
JSON, e os quatro emitem os mesmos bytes.
Regra 5 — Preservação de null¶
Um valor JSON null deve (MUST) ser preservado com a sua chave:
Um null dentro de um array preserva a sua posição:
Regra 6 — Tratamento de null e undefined (entre linguagens)¶
O JSON tem um único canal de valor ausente (null). As linguagens hospedeiras têm mais.
O TypeScript é a única linguagem hospedeira que distingue undefined
de null; Python, Go e Rust não. O canonicalizador deve (MUST) tratar
cada valor do hospedeiro assim:
| Linguagem hospedeira | Valor do hospedeiro | Em uma chave de objeto | Em uma posição de array |
|---|---|---|---|
| TypeScript | undefined |
omitir a chave | emitir null |
| TypeScript | null |
emitir null |
emitir null |
| Python | chave ausente | omitir a chave | n/a |
| Python | None |
emitir null |
emitir null |
| Go | entrada de mapa ausente | omitir a chave | n/a |
| Go | interface nil / null JSON explícito |
emitir null |
emitir null |
| Rust | campo de struct ausente com skip_serializing_if=None |
omitir a chave | n/a |
| Rust | Option::None serializado sem skip |
emitir null |
emitir null |
| Rust | Value::Null |
emitir null |
emitir null |
input → { a: 1, b: undefined, c: 3 } (TS)
output → {"a":1,"c":3}
input → [1, undefined, 3] (TS)
output → [1,null,3]
input → {"a": 1, "b": None, "c": 3} (Python)
output → {"a":1,"b":null,"c":3}
Isso é assimétrico de propósito: no TypeScript, um objeto com uma
chave ausente e um objeto com b: undefined produzem a mesma saída,
de acordo com a incapacidade do JSON de expressar separadamente “ausente versus valor
undefined”. Dentro de um array a posição tem significado, então
undefined se materializa como null.
Os SDKs não devem (MUST NOT) inventar um canal undefined onde a linguagem hospedeira não tem
nenhum. None do Python, nil do Go de um tipo interface que produziu um null JSON
na análise, e Value::Null do Rust são todos mapeados para JSON null.
Regra 7 — Booleanos¶
Os booleanos são emitidos como os literais true e false.
Regra 8 — Contêineres vazios¶
Um objeto vazio emite {}. Um array vazio emite []. Nenhum dos dois é
omitido.
Regra 9 — A normalização Unicode está FORA DO ESCOPO¶
As strings são emitidas como a sua sequência de bytes UTF-8 com as regras de
escape da regra 3, sem nenhuma normalização Unicode (NFC, NFD,
NFKC, NFKD). Um produtor que fornece a string pré-composta "é"
(U+00E9) e um produtor que fornece a sequência decomposta
"e" + U+0301 produzirão bytes diferentes, embora as
duas strings sejam exibidas da mesma forma e sejam iguais sob a equivalência
canônica.
É uma não decisão deliberada: exigir uma forma de normalização na camada de canonicalização obrigaria todo SDK a trazer uma dependência equivalente a ICU ou a tabelas Unicode, e ainda deixaria em aberto qual forma (NFC ou NFKC) vence. As aplicações que se preocupam com a resistência a homógrafos devem (MUST) normalizar as suas strings antes de entregar o valor ao canonicalizador (NFC é a escolha convencional para o intercâmbio de JSON no fio — RFC 8259 § 8.1).
Um perfil futuro (dcp-jcs-v2 ou posterior) pode (MAY) fixar uma forma de normalização;
essa decisão ficará no seu próprio documento de perfil.
Nota sobre
NaN/±Infinity: esses valores não fazem parte do formato de transmissão JSON da RFC 8259. A maioria dos parsers JSON os rejeita na análise, então o canonicalizador nunca os vê. Onde um parser os aceita (por exemplo,json.loads("NaN")do Python, que os aceita por padrão, ou valores construídos à mão em JavaScript), a regra 4 continua a rejeitá-los após a análise. Os fixtures de interoperabilidade deliberadamente não trazem um vetor NaN/Infinity porque nenhuma sintaxe JSON portátil consegue expressá-los; a rejeição é verificada por testes unitários de cada SDK (por exemplo,sdks/rust/src/v2/canonicalize.rstest_rejects_non_finite).
3. Tabela de aceitação de casos-limite¶
Estes são os casos que divergiam entre SDKs antes de o perfil ser formalizado. Um SDK que cumpre o perfil deve (MUST) coincidir com cada célula desta tabela.
| Entrada (literal JSON) | Saída | Notas |
|---|---|---|
null |
null |
Regra 5 |
true |
true |
Regra 7 |
false |
false |
Regra 7 |
0 |
0 |
Regra 4 |
-0 |
0 |
Regra 4, sinal normalizado |
1 |
1 |
Regra 4 |
1.0 |
1 |
Regra 4, float de valor inteiro |
1.00 |
1 |
Regra 4, zeros finais |
1e2 |
100 |
Regra 4, notação científica com valor inteiro |
100 |
100 |
Regra 4 |
-42 |
-42 |
Regra 4 |
0.1 |
ERROR | Regra 4, valor fracionário |
1.5 |
ERROR | Regra 4, valor fracionário |
1.0e-1 |
ERROR | Regra 4, valor fracionário |
NaN |
ERROR | Regra 4, não finito |
Infinity |
ERROR | Regra 4, não finito |
{} |
{} |
Regra 8 |
[] |
[] |
Regra 8 |
{"x": null, "y": 1} |
{"x":null,"y":1} |
Regra 5 |
[1, null, 3] |
[1,null,3] |
Regra 5 |
{"é": 1, "e": 2, "z": 3} |
{"e":2,"z":3,"é":1} |
Regra 1, ordem por ponto de código |
{"a": {"b": {"c": 42}}} |
{"a":{"b":{"c":42}}} |
Regra 1, recursivo |
{"fi": 1, "😀": 2, "a": 3} |
{"a":3,"fi":1,"😀":2} |
Regra 1, ordem por ponto de código: U+FB01 < U+1F600 (a ordem por unidades de código UTF-16 poria o emoji primeiro) |
{"s": "<a href='/x?y=1&z=2'>"} |
{"s":"<a href='/x?y=1&z=2'>"} |
Regra 3, <, > e & não são escapados |
{"s": "a
b
c"} |
{"s":"a U+2028 b U+2029 c"} como UTF-8 sem escape |
Regra 3, os separadores de linha e de parágrafo não são escapados |
{"s": "\u0000\u0008\t\n\u000b\f\r\u001f\"\\"} |
{"s":"\u0000\b\t\n\u000b\f\r\u001f\"\\"} |
Regra 3, escapes curtos da RFC 8785 §3.2, \u00xx em minúsculas para os demais controles de C0 |
4. Declaração no manifesto do bundle¶
Os bundles produzidos sob este perfil deveriam (SHOULD) incluir um campo de nível superior
canonicalization_profile no manifesto:
{
"manifest": {
"canonicalization_profile": "dcp-jcs-v1",
"session_nonce": "...",
"rpr_hash": "sha256:...",
...
}
}
O campo é opcional por compatibilidade com os bundles produzidos
antes de o perfil existir. Os verificadores que encontram um bundle sem
o campo devem (MUST) assumir dcp-jcs-v1 (porque ainda não existe nenhum outro
perfil). Quando perfis futuros forem publicados (dcp-jcs-v2, ...), o campo passará
a ser obrigatório para desambiguar.
O campo é acrescentado a schemas/v2/bundle_manifest.schema.json:
"canonicalization_profile": {
"type": "string",
"const": "dcp-jcs-v1",
"description": "Identifier of the canonicalization profile under which this bundle was produced."
}
5. Versionamento¶
Este documento define o perfil dcp-jcs-v1, que está congelado: qualquer
mudança nas regras acima exige um novo identificador de perfil. A
governança para acrescentar um perfil novo:
- Uma mudança que admite entradas novas ou normaliza de outra forma → perfil
novo (por exemplo,
dcp-jcs-v2). - Uma mudança que só endurece a validação (rejeitando entradas que antes eram aceitas) → avalie se os verificadores a jusante podem se atualizar ao mesmo tempo; se não, exige um perfil novo.
- Um esclarecimento que documenta o comportamento existente sem mudá-lo → atualização no local, registrada no CHANGELOG com o mesmo identificador de perfil.
Quando um perfil futuro for publicado:
- O perfil novo terá o seu próprio documento em
spec/(por exemplo,CANONICALIZATION_PROFILE_V2.md). - Os SDKs publicarão a sua matriz de suporte nos seus README.
- O
canonicalization_profiledo manifesto do bundle passará a ser um campo obrigatório, e os verificadores roteiam para o canonicalizador adequado conforme o seu valor.
6. Vetores de conformidade¶
O arquivo compartilhado tests/interop/v2/interop_vectors.json contém um
bloco canonicalization.edge_cases que materializa o §3 como um conjunto de
fixtures de teste. Os quatro SDKs (TypeScript, Python, Rust, Go) devem (MUST) passar
neste bloco para afirmar que cumprem o perfil.
Os vetores são consumidos pelos testes de interoperabilidade de cada SDK:
sdks/typescript/src/__tests__/interop.test.tssdks/python/tests/test_interop.pysdks/rust/tests/interop.rssdks/go/dcp/interop_test.go
A CI executa os quatro em paralelo em cada PR. Uma divergência entre SDKs bloqueia uma versão.
7. Referências¶
- RFC 8785 — JSON Canonicalization Scheme (JCS) — especificação de base.
- RFC 8259 — JSON Data Interchange Format — gramática em nível de fio.
spec/DCP-AI-v2.0.md§ 15 — resumo em nível de protocolo.schemas/v2/bundle_manifest.schema.json— declaração do campo do manifesto.tests/interop/v2/interop_vectors.json#canonicalization.edge_cases— fixtures de teste.
Notas de implementação (informativas)¶
Acrescentadas em 25/10/2026. Não normativas. Estas notas não acrescentam nenhum requisito nem mudam nada do que está acima: descrevem como o código de referência em
mainse relaciona com este texto. Onde divergem, prevalece o texto normativo acima.
dcp-jcs-v1continua congelado (§5) e continua sendo o único perfil de canonicalização normativo e o padrão. O identificadordcp-jcs-v2nomeia um candidato não normativo, descrito em docs/proposals/canonicalization-candidate, com APIs experimentais de ativação explícita nos SDKs que nenhum caminho padrão chama. O schema do manifesto continua fixandocanonicalization_profileemdcp-jcs-v1, e os verificadores V2 existentes rejeitam o identificador do candidato.- A regra 4 é decidida sobre o valor analisado, então o que um produtor ou um verificador canonicaliza depende do seu parser JSON.
Os parsers divergem em entradas que este perfil não fixa: nomes de membro repetidos, escapes de surrogates isolados, uma marca
de ordem de bytes inicial e literais numéricos que um parser binary64 arredonda (por exemplo,
1.0000000000000001é lido como1). Portanto, dois parsers podem canonicalizar o mesmo texto de forma diferente; a proposta do candidato registra esses casos. - O exemplo da nota sob a regra 9 foi corrigido em 25/10/2026:
json.loadsnão tem nenhum argumentoallow_nan(ele aceitaNaNpor padrão);allow_nanpertence ajson.dumps. Nenhuma regra mudou.