CBOR no .NET: o que é e como usar na prática

CBOR no .NET: o que é e como usar na prática

Usar CBOR no .NET permite trocar dados estruturados em formato binário, de forma compacta, padronizada e autodescritiva. Neste artigo, você vai entender o que é o formato, como ele se compara a outras opções e como ler e gravar dados com o pacote System.Formats.Cbor.

Por que pensar além do JSON

Serializar dados significa transformar uma estrutura em memória em uma sequência de bytes. Depois disso, esses bytes podem trafegar pela rede ou ficar salvos em disco até alguém reconstruir a estrutura original.

Hoje, o JSON é o formato mais popular para essa tarefa, bem mais comum que o CBOR em aplicações .NET. Ele é texto puro em UTF-8, então qualquer pessoa consegue abrir um payload e entender o conteúdo.

Além disso, o JSON é autodescritivo. Ou seja, cada valor carrega o nome do seu campo, e um decodificador genérico consegue ler o documento sem conhecer o contrato antecipadamente.

{
  "device": "sensor-01",
  "battery": 87,
  "online": true,
  "readings": [21, 22, 24],
  "location": {
    "room": "lab"
  }
}

No entanto, essa legibilidade tem um custo quando os dados não são texto. Um booleano, por exemplo, ocupa quatro ou cinco bytes (true ou false) para representar um único bit de informação.

Da mesma forma, números também viram texto. Por isso, o decodificador precisa interpretar caractere por caractere antes de chegar ao valor numérico.

O problema fica maior com dados binários. Como o JSON não tem um tipo nativo para bytes, a saída comum é converter tudo para Base64, o que aumenta o tamanho em cerca de 33%.

Em outras palavras, cada três bytes originais viram quatro caracteres. Além do tráfego maior, o lado que recebe ainda precisa decodificar a string antes de usar os bytes.

É exatamente aqui que o CBOR no .NET entra. Ele mantém o modelo de dados do JSON, mas troca o texto por uma representação binária enxuta.

O que é CBOR

CBOR é a sigla de Concise Binary Object Representation. Trata-se de um padrão da IETF, descrito hoje na RFC 8949, que substituiu a RFC 7049 original.

Algumas fontes chamam o formato de Compact Binary Object Notation, mas o nome oficial é o da especificação. Na prática, os dois termos se referem à mesma tecnologia, e o pacote de CBOR no .NET segue a RFC 8949.

O padrão nasceu com objetivos bem claros:

  • Representar os mesmos tipos de dados do JSON, com suporte nativo a dados binários.
  • Manter o código do codificador e do decodificador o mais simples possível.
  • Gerar mensagens menores que o JSON equivalente.
  • Continuar autodescritivo, assim como JSON e MessagePack, e diferente do Protobuf.

O público inicial eram cenários de Internet das Coisas, com dispositivos de pouca memória e pouca energia. Mesmo assim, as mesmas vantagens servem para qualquer aplicação que troca muitos dados entre processos, como serviços em .NET que usam CBOR internamente.

Por esse motivo, o formato aparece em protocolos importantes. O CoAP usa CBOR em IoT, e o padrão FIDO2 também depende dele no protocolo CTAP, utilizado por chaves de segurança e passkeys.

Como os bytes do CBOR se organizam

Cada item de um documento CBOR começa com um byte de cabeçalho. Esse cabeçalho indica o tipo principal do item e, em muitos casos, já carrega o próprio valor ou o tamanho do conteúdo.

Por exemplo, inteiros pequenos cabem inteiros no cabeçalho. Assim, o número 8 ocupa um único byte (08), enquanto o valor true vira apenas F5.

Mapas e arrays informam a quantidade de itens logo no cabeçalho. Em seguida, cada elemento da coleção traz o seu próprio cabeçalho, e assim por diante.

Strings também declaram o tamanho em bytes antes do conteúdo, e o leitor de CBOR no .NET aproveita essa informação. Com isso, o decodificador sabe quanto memória reservar e não precisa procurar aspas nem tratar sequências de escape.

Além disso, o padrão aceita strings, mapas e arrays de tamanho indefinido. Esse recurso atende cenários de streaming, quando quem envia ainda não sabe quantos itens vão existir.

Para leitura humana, existe a Concise Diagnostic Notation, uma notação parecida com JSON. Mesmo assim, o mais comum é ver o hexadecimal comentado, como no trecho abaixo, que representa o início do objeto de telemetria.

A5                          # map(5)
   66                       # text(6)
      646576696365          # "device"
   69                       # text(9)
      73656E736F722D3031    # "sensor-01"
   67                       # text(7)
      62617474657279        # "battery"
   18 57                    # unsigned(87)
   66                       # text(6)
      6F6E6C696E65          # "online"
   F5                       # true

O ganho de tamanho depende do tipo de dado. Números pequenos, booleanos e bytes ficam muito menores em binário, porém textos têm tamanho parecido, já que ambos os formatos usam UTF-8.

CBOR, Protobuf e MessagePack

Quem estuda formatos binários logo esbarra em duas alternativas famosas. São elas o Protocol Buffers, mais conhecido como Protobuf, e o MessagePack.

O Protobuf nasceu no Google e é o formato de troca padrão do gRPC. Ele também é binário, mas depende de arquivos .proto compartilhados que descrevem cada mensagem.

Esse contrato prévio permite payloads muito pequenos e codificadores muito rápidos. Por outro lado, ele cria um acoplamento forte entre produtor e consumidor, que precisam manter o mesmo esquema sincronizado.

Em ambientes controlados, esse acoplamento funciona bem. Contudo, em integrações abertas, o modelo autodescritivo do CBOR no .NET oferece mais flexibilidade.

Já o MessagePack resolve praticamente o mesmo problema que o CBOR. Ele também é uma alternativa binária ao JSON, com os mesmos tipos e suporte extra a bytes.

As diferenças principais são duas. Primeiro, o CBOR tem um padrão formal da IETF, enquanto o MessagePack segue uma especificação mantida pela comunidade.

Além disso, o CBOR oferece suporte nativo a itens de tamanho indefinido, o que ajuda em streaming. De certa forma, ele funciona como uma versão padronizada da ideia que o MessagePack popularizou anos antes, e hoje o .NET oferece suporte oficial apenas ao CBOR.

Característica JSON CBOR MessagePack Protobuf
Formato Texto Binário Binário Binário
Autodescritivo Sim Sim Sim Não
Tipo binário nativo Não Sim Sim Sim
Padrão formal RFC 8259 RFC 8949 Especificação própria Padrão de fato
Streaming com tamanho indefinido Não Sim Não Não

Como instalar o suporte a CBOR no .NET

O suporte oficial a CBOR no .NET vem do pacote System.Formats.Cbor, mantido pela própria Microsoft. Ele funciona no .NET 10, versão LTS atual, e também atende projetos em .NET Framework e .NET Standard 2.0.

Para adicionar o pacote ao projeto, basta executar o comando abaixo.

dotnet add package System.Formats.Cbor

O pacote tem dois tipos centrais. O CborWriter grava valores em um buffer no formato binário, enquanto o CborReader lê dados codificados a partir de um ReadOnlyMemory<byte>.

Vale um alerta antes de começar. Essas APIs são de baixo nível e não oferecem algo como o JsonSerializer do System.Text.Json, que converte objetos automaticamente.

Na prática, você escreve e lê valores primitivos um a um. Isso dá mais trabalho, porém entrega controle total sobre cada byte e evita alocações desnecessárias.

A documentação do namespace System.Formats.Cbor lista ainda tipos auxiliares, como CborReaderState, CborTag e CborContentException.

Gravando CBOR no .NET com CborWriter

O exemplo a seguir grava o mesmo objeto de telemetria apresentado no início. Cada par de chave e valor vira duas chamadas ao CborWriter, uma para a chave e outra para o valor.

using System.Formats.Cbor;

var writer = new CborWriter();

// Informamos antecipadamente que o mapa terá cinco pares
writer.WriteStartMap(5);

writer.WriteTextString("device");
writer.WriteTextString("sensor-01");

writer.WriteTextString("battery");
writer.WriteInt32(87);

writer.WriteTextString("online");
writer.WriteBoolean(true);

writer.WriteTextString("readings");
writer.WriteStartArray(3);
writer.WriteInt32(21);
writer.WriteInt32(22);
writer.WriteInt32(24);
writer.WriteEndArray();

writer.WriteTextString("location");
writer.WriteStartMap(1);
writer.WriteTextString("room");
writer.WriteTextString("lab");
writer.WriteEndMap();

writer.WriteEndMap();

// Gera o array de bytes final
byte[] payload = writer.Encode();

Console.WriteLine($"Size: {payload.Length} bytes");
Console.WriteLine(Convert.ToHexString(payload));

Ao executar o código, o resultado tem 69 bytes. O mesmo objeto em JSON minificado ocupa 97 bytes, ou seja, o CBOR no .NET gerou um payload cerca de 29% menor.

Size: 69 bytes
A5666465766963656973656E736F722D303167626174746572791857666F6E6C696E65F56872656164696E67738315161818686C6F636174696F6EA164726F6F6D636C6162

Alguns detalhes desse código merecem atenção. Primeiro, sempre que souber o tamanho de um mapa ou array, informe esse número na abertura.

Caso contrário, o escritor cria um item de tamanho indefinido. Isso funciona, mas consome um byte extra de marcação no final e algumas leituras ficam menos diretas.

Além disso, as chaves de um mapa podem ser de qualquer tipo. Neste exemplo usamos apenas strings, porém inteiros como chave reduzem ainda mais o tamanho da mensagem.

O CborWriter guarda tudo em um buffer interno até você chamar Encode() ou TryEncode(). O primeiro devolve um byte[], e o segundo grava em um Span<byte> fornecido por você.

Por fim, o escritor protege a estrutura do documento. Se você fechar um mapa que não foi aberto ou chamar Encode() com um documento incompleto, ele lança uma exceção.

Modos de conformidade do CBOR no .NET

O construtor do CborWriter aceita alguns parâmetros opcionais. Entre eles estão conformanceMode, convertIndefiniteLengthEncodings, allowMultipleRootLevelValues e initialCapacity.

O mais interessante é o modo de conformidade, definido pelo enum CborConformanceMode. Ele controla quais regras o CBOR no .NET valida durante a escrita e a leitura.

  • Lax: aceita qualquer documento bem formado.
  • Strict: o padrão, que proíbe chaves duplicadas em mapas e exige UTF-8 válido.
  • Canonical: exige inteiros na menor codificação possível, chaves ordenadas e nenhum item de tamanho indefinido.
  • Ctap2Canonical: segue as regras canônicas do padrão CTAP2, usado pelo FIDO2, e proíbe tags.

Para ativar o modo canônico, basta passar o valor no construtor.

var writer = new CborWriter(CborConformanceMode.Canonical);

Nesse modo, o escritor ordena as chaves pela codificação. Assim, o mesmo conteúdo sempre gera exatamente os mesmos bytes, o que é essencial para assinaturas digitais e comparações por hash.

Lendo CBOR no .NET com CborReader

Com o payload em mãos, o CborReader faz o caminho inverso. Ele é um leitor com estado, que avança sempre para frente, item por item.

Como a API é de baixo nível, existem várias formas de escrever o decodificador. O exemplo abaixo preenche uma classe Telemetry e usa PeekState() para descobrir quando o mapa termina.

using System.Formats.Cbor;

var reader = new CborReader(payload);
var telemetry = new Telemetry();

reader.ReadStartMap();

while (reader.PeekState() != CborReaderState.EndMap)
{
    var key = reader.ReadTextString();

    switch (key)
    {
        case "device":
            telemetry.Device = reader.ReadTextString();
            break;

        case "battery":
            telemetry.Battery = reader.ReadInt32();
            break;

        case "online":
            telemetry.Online = reader.ReadBoolean();
            break;

        case "readings":
            reader.ReadStartArray();
            while (reader.PeekState() != CborReaderState.EndArray)
                telemetry.Readings.Add(reader.ReadInt32());
            reader.ReadEndArray();
            break;

        default:
            // Campo desconhecido: pulamos o valor sem quebrar a leitura
            reader.SkipValue();
            break;
    }
}

reader.ReadEndMap();

Console.WriteLine($"{telemetry.Device}: {telemetry.Battery}% ({telemetry.Readings.Count} readings)");

public class Telemetry
{
    public string Device { get; set; } = string.Empty;
    public int Battery { get; set; }
    public bool Online { get; set; }
    public List<int> Readings { get; } = [];
}

O laço com PeekState() tem uma vantagem importante. Ele funciona tanto para mapas de tamanho definido quanto para mapas de tamanho indefinido, sem depender do contador retornado por ReadStartMap().

Em seguida, cada chave decide qual método de leitura chamar. Se o tipo esperado não bater com o dado real, o leitor lança uma InvalidOperationException ou uma CborContentException.

Esse código claramente exige mais trabalho que um JsonSerializer.Deserialize<T>(). Por outro lado, quando você conhece o formato dos dados, dificilmente existe uma forma mais eficiente de lê-los.

Também é possível reduzir alocações. Com TryReadTextString, por exemplo, você lê a chave em um Span<char> reaproveitado, sem criar uma nova string a cada campo.

Compatibilidade com campos desconhecidos

Repare que o decodificador não trata a chave location. Mesmo assim, a leitura continua normalmente, porque o bloco default chama SkipValue() e pula o valor inteiro, inclusive o mapa aninhado.

Essa é uma forma simples de garantir compatibilidade futura. Ou seja, uma versão antiga do consumidor consegue ler mensagens de um produtor mais novo, que já envia campos adicionais.

Se você quiser guardar o valor desconhecido em vez de descartá-lo, use ReadEncodedValue(). Esse método devolve os bytes originais do item, que podem ser registrados em log ou repassados adiante.

Além disso, o PeekState() permite montar um decodificador totalmente genérico. Como ele informa o tipo do próximo item sem consumi-lo, você descobre qual método Read* deve chamar em cada passo.

Quando usar CBOR no .NET

A resposta honesta é: depende do cenário. Para a maioria das APIs web, o JSON continua sendo a melhor escolha, porque é legível, universal e muito bem suportado pelo ecossistema.

No entanto, existem casos em que o CBOR no .NET faz bastante sentido:

  • Dispositivos IoT ou redes com banda limitada, onde cada byte conta.
  • Mensagens com muitos números, booleanos ou dados binários, como hashes e assinaturas.
  • Integrações com protocolos que já exigem o formato, como WebAuthn, FIDO2 e COSE.
  • Dados internos de uma única aplicação, que nunca serão lidos por pessoas.
  • Cenários que precisam de bytes determinísticos, graças ao modo canônico.

Por outro lado, evite o formato quando a depuração manual é frequente ou quando parceiros externos esperam JSON. Nesses casos, o ganho de tamanho raramente compensa o custo de integração.

Outro ponto de atenção é a segurança. Assim como acontece com tokens como o JWT, que você conhece melhor no artigo o que é JWT, todo dado recebido de fora precisa de validação antes do uso.

O exemplo do ASP.NET Core com CBOR

Um uso real do formato aparece no próprio ASP.NET Core. O suporte a Device Bound Session Credentials (DBSC), previsto para o .NET 11, usa o CborWriter para montar um desafio que depois vai para um cookie.

Nesse fluxo, o framework grava dois valores de texto, protege o resultado com a Data Protection API e codifica tudo em Base64 URL. O trecho abaixo reproduz a ideia com nomes simplificados.

public string CreateChallenge(string userId, string sessionId, TimeSpan lifetime)
{
    // Vários valores na raiz, sem mapa de chave e valor
    var writer = new CborWriter(allowMultipleRootLevelValues: true);
    writer.WriteTextString(userId);
    writer.WriteTextString(sessionId);

    var protectedBytes = _protector.Protect(writer.Encode(), lifetime);
    return WebEncoders.Base64UrlEncode(protectedBytes);
}

O detalhe curioso é que o código não tenta imitar o JSON. Em vez de um mapa com nomes de campos, ele grava dois valores soltos na raiz, que formam uma versão binária de <userId><sessionId>.

Como apenas a mesma aplicação lê esses bytes, não existe motivo para gastar espaço com nomes de campos. Para ler, basta criar o leitor com new CborReader(bytes, allowMultipleRootLevelValues: true) e chamar ReadTextString() duas vezes.

Esse caso mostra bem onde o CBOR no .NET brilha. Ele oferece um formato binário padronizado, validado e compacto, sem que você precise inventar a própria serialização manual.

Conclusão

O CBOR resolve dois pontos fracos do JSON: o tamanho excessivo de dados não textuais e a ausência de um tipo binário nativo. Além disso, ele mantém o modelo autodescritivo e conta com um padrão formal da IETF.

Neste artigo, você viu como o formato organiza os bytes e como ele se compara ao Protobuf e ao MessagePack. Também viu como gravar e ler dados com CborWriter e CborReader.

Em resumo, o CBOR no .NET não substitui o JSON nas suas APIs do dia a dia. Contudo, ele é uma ferramenta valiosa para IoT, segurança, dados binários e formatos internos, sempre que cada byte e cada alocação importam.

FAQ

O que significa CBOR?

CBOR significa Concise Binary Object Representation. É um formato binário de serialização padronizado pela IETF na RFC 8949.

O CBOR é melhor que o JSON?

Depende do cenário. O CBOR gera mensagens menores e suporta bytes nativamente, enquanto o JSON é legível e mais universal.

Preciso instalar algum pacote para usar CBOR no .NET?

Sim. O suporte oficial vem do pacote System.Formats.Cbor, disponível no NuGet.

O System.Formats.Cbor converte objetos automaticamente?

Não. O pacote oferece apenas APIs de baixo nível para ler e gravar valores primitivos, sem serialização automática de classes.

Qual a diferença entre CBOR e MessagePack?

Os dois têm objetivos parecidos. O CBOR, porém, tem padrão formal da IETF e suporte a itens de tamanho indefinido para streaming.

Quando devo usar o modo canônico?

Use o modo canônico quando precisar que o mesmo conteúdo gere sempre os mesmos bytes, como em assinaturas digitais e comparações por hash.