orjson é uma biblioteca de serialização JSON voltada a baixa latência e suporte nativo a tipos comuns, como dataclasses, datas e UUIDs. Ela não deve ser uma troca automática: o contrato difere do módulo json, e desempenho precisa ser medido no fluxo real.

Diferença essencial: bytes

python -m pip install orjson
from datetime import datetime, timezone
import orjson

dados = {"evento": "acesso", "quando": datetime.now(timezone.utc)}
conteudo: bytes = orjson.dumps(dados, option=orjson.OPT_UTC_Z)
recuperado = orjson.loads(conteudo)

dumps() retorna bytes, não str. Isso combina bem com corpos HTTP e arquivos binários, mas pode quebrar código que concatena texto. Não aplique .decode() e .encode() sem necessidade, pois conversões extras reduzem o benefício.

Tipos e opções explícitas

Para tipos próprios, forneça default e lance TypeError quando não souber converter:

from decimal import Decimal


def default(obj):
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError


payload = orjson.dumps({"total": Decimal("19.90")}, default=default)

Decidir que Decimal vira string é parte do contrato da API. Registre essa escolha e teste a ida e volta. O guia de JSON em Python ajuda a revisar limitações do formato, enquanto dataclasses mostra modelos serializados nativamente pelo orjson.

Migrar com segurança

Crie testes de compatibilidade com payloads reais: Unicode, datas, inteiros grandes, chaves não textuais e valores não finitos. Compare conteúdo, não apenas velocidade. Um benchmark deve incluir o custo total, como leitura, validação e envio, usando dados representativos e várias execuções.

A documentação oficial do orjson, consultada em 22 de julho de 2026, descreve tipos, opções e diferenças de migração. Preserve json quando ele já atende ao requisito; adote orjson quando a medição justificar a dependência e o contrato em bytes estiver claro.

Ler e escrever no limite correto

orjson.loads() aceita bytes, bytearray, memoryview e texto UTF-8. Ele devolve os tipos JSON usuais: dicionário, lista, string, inteiro, float, booleano e None. A desserialização não cria automaticamente dataclasses, datas ou UUIDs. Essa reconstrução pertence a uma camada de validação posterior.

Ao gravar em um arquivo aberto em modo binário, os bytes retornados por dumps() podem ser escritos diretamente:

from pathlib import Path
import orjson


def salvar_evento(caminho: Path, evento: dict) -> None:
    caminho.write_bytes(orjson.dumps(evento))


def carregar_evento(caminho: Path) -> dict:
    valor = orjson.loads(caminho.read_bytes())
    if not isinstance(valor, dict):
        raise ValueError("o documento deve ser um objeto JSON")
    return valor

Em APIs HTTP, confirme o contrato do framework. Alguns aceitam bytes como corpo; outros esperam um objeto Python e executam sua própria serialização. Não serialize duas vezes, pois o consumidor receberia uma string JSON contendo JSON escapado.

Datas e política de timezone

orjson serializa datetime, date e time, mas a representação exata depende do valor e das opções. Defina uma política de aplicação:

from datetime import datetime, timezone
import orjson

evento = {
    "criado_em": datetime(2026, 8, 21, 15, 0, tzinfo=timezone.utc),
}

payload = orjson.dumps(
    evento,
    option=orjson.OPT_UTC_Z,
)
assert b"2026-08-21T15:00:00Z" in payload

OPT_NAIVE_UTC trata datas ingênuas como UTC. Só habilite essa opção se esse significado for garantido no sistema; ela pode esconder uma data local sem timezone. OPT_OMIT_MICROSECONDS remove precisão, portanto também é uma decisão de contrato. Teste valores com e sem microssegundos e rejeite misturas ambíguas antes da serialização.

Dataclasses, enums e UUID

Dataclasses são suportadas nativamente, mas todos os campos serializáveis podem entrar na saída. Isso não significa que a estrutura interna deva virar sua API pública. Um novo campo operacional pode aparecer no JSON após uma alteração aparentemente inocente.

Crie um dicionário explícito quando precisar de nomes estáveis, controle de versão ou exclusão de segredos. Enums e UUIDs também têm representações documentadas; mantenha testes de contrato para o formato que consumidores esperam. O default serve para tipos não suportados, não para interceptar arbitrariamente todo tipo já tratado de forma nativa.

Chaves e ordenação

JSON define chaves de objeto como strings. OPT_NON_STR_KEYS permite algumas chaves não textuais, convertendo-as. Essa conveniência pode causar colisão: chaves distintas em Python podem produzir a mesma chave textual. Prefira normalizar e validar o dicionário antes.

OPT_SORT_KEYS gera ordem determinística, útil para snapshots ou comparação humana, mas custa processamento. Ordem não deve ser usada como semântica de um objeto JSON. Para assinaturas criptográficas ou canonicalização, siga uma especificação própria; apenas ordenar chaves não garante JSON canônico.

Inteiros, floats e interoperabilidade

Python aceita inteiros arbitrariamente grandes, enquanto muitos consumidores representam números com precisão limitada. OPT_STRICT_INTEGER ajuda a rejeitar inteiros fora da faixa interoperável documentada pelo orjson. Escolha entre número e string no contrato, especialmente para identificadores.

Valores não finitos como NaN e infinito não fazem parte do JSON padrão. Não suponha que orjson reproduzirá exatamente a política do módulo json. Inclua esses valores nos testes de migração e valide dados científicos antes de publicá-los.

Na leitura, entradas inválidas levantam orjson.JSONDecodeError; na escrita, valores sem representação levantam JSONEncodeError. Capture erros apenas no limite em que seja possível acrescentar contexto ou convertê-los em uma resposta adequada. Não esconda a causa com um except Exception.

Formatação e logs

O padrão compacto é apropriado para rede e armazenamento. OPT_INDENT_2 facilita arquivos destinados a pessoas, enquanto OPT_APPEND_NEWLINE ajuda no formato de um documento por linha. Para JSON Lines, serialize cada registro separadamente e garanta que strings internas sejam corretamente escapadas:

def linha_json(registro: dict) -> bytes:
    return orjson.dumps(registro, option=orjson.OPT_APPEND_NEWLINE)

Não use serialização rápida como licença para registrar payloads completos. Remova tokens, dados pessoais e segredos antes do log. Limites de tamanho continuam necessários contra consumo excessivo de memória.

Benchmark representativo

Meça com o mesmo Python, máquina e payload usados pela aplicação. Separe aquecimento, execute várias repetições e compare mediana ou distribuição. Inclua serialização, desserialização e eventuais conversões para texto. Um exemplo pequeno com timeit serve como triagem:

from timeit import repeat
import json
import orjson

dados = [{"id": n, "ativo": True, "nome": f"item-{n}"} for n in range(1000)]

tempos_json = repeat(lambda: json.dumps(dados), number=100, repeat=5)
tempos_orjson = repeat(lambda: orjson.dumps(dados), number=100, repeat=5)

print(min(tempos_json), min(tempos_orjson))

Não publique essa razão como verdade universal. Payload, CPU, versão e opções alteram o resultado. Se banco, rede ou validação dominam a latência, reduzir a serialização pode não mudar a experiência.

Checklist de migração

  • confirme se cada chamador espera str ou bytes;
  • compare Unicode, escapes, datas, decimais, UUIDs e enums;
  • teste inteiros grandes, floats não finitos e chaves não textuais;
  • verifique mensagens e tipos de exceção usados pela aplicação;
  • examine integração com framework, cache, arquivo e fila;
  • mantenha fixtures de payload consumidas por outros serviços;
  • execute benchmark do caminho completo;
  • documente opções e a política de compatibilidade.

Uma pequena função adaptadora pode concentrar opções e tratamento de erro. Isso evita que cada endpoint escolha uma política diferente e facilita voltar ao módulo padrão se a dependência deixar de compensar.

Segurança e compatibilidade operacional

Trate JSON como entrada não confiável. orjson faz parsing, mas não impõe schema, autorização nem limites do protocolo. Restrinja o tamanho do corpo antes de carregá-lo, valide profundidade e quantidade de itens conforme o domínio e rejeite campos inesperados quando o contrato exigir. Desempenho não elimina risco de negação de serviço.

Ao atualizar a dependência, leia as notas de versão e execute fixtures de compatibilidade. Wheels e requisitos de plataforma podem diferir dos do módulo padrão, então confirme a disponibilidade no ambiente de build e produção. Nunca dependa de uma compilação manual feita apenas no servidor.

Separe o benchmark do teste funcional. O CI deve confirmar o formato e os erros de maneira determinística; limites rígidos de tempo costumam oscilar em máquinas compartilhadas. Acompanhe desempenho em um ambiente controlado ou use regressões amplas com contexto suficiente para investigar.

Na revisão de código, torne cada opção legível. Uma máscara com muitas constantes pode ser encapsulada em uma função com nome de domínio, acompanhada por testes. Quem mantém o sistema precisa entender por que UTC recebe Z, por que microssegundos são preservados e quais tipos podem aparecer, sem reconstruir decisões a partir de uma expressão numérica.