JSON para classe Java: o gerador entrega o esqueleto, a produção exige mais cinco coisas
Resumo: Um olhar prático sobre o que um gerador JSON → Java realmente emite——números viram Double, objetos aninhados viram classes internas estáticas, e nenhuma anotação Jackson é gerada. Precisão monetária, renomeação de campos, apagamento de genéricos, campos desconhecidos e datas são as cinco coisas que você mesmo precisa adicionar, com código pronto para copiar em cada caso.

Resumo: a classe gerada compila imediatamente, mas não está pronta para produção. Os padrões medidos do nosso gerador: number→Double, objetos aninhados→classes internas estáticas, nenhuma anotação Jackson. Precisão monetária, renomeação de campos, apagamento de genéricos, campos desconhecidos e datas — cinco coisas que ele jamais fará por você.
Transformar uma resposta de API em classes Java é trabalho mecânico: ler o JSON, copiar os campos, envolvê-los em getters e setters. O preço do trabalho mecânico é a faca invisível — um nome de propriedade copiado errado não quebra a compilação. O campo chega silenciosamente como null, e você descobre num log de produção. Gerar a estrutura da classe existe para alinhar nomes e tipos desde o primeiro dia. Mas o gerador só consegue alinhar o que é visível na amostra.
1. Rode primeiro o gerador: o que ele realmente entrega
Cole uma resposta representativa na nossa ferramenta JSON para Java:
{
"order_id": 202611040001,
"buyer": {
"name": "王五",
"vip": true
},
"items": [
{ "sku": "A-1", "price": 19.99, "qty": 2 }
],
"created_at": "2026-11-04T09:30:00+08:00"
}
O esqueleto gerado (opções padrão, useLombok desativado):
public class Root {
private Double orderId;
private Buyer buyer;
private List<Item> items;
private String createdAt;
public Double getOrderId() { return orderId; }
public void setOrderId(Double orderId) { this.orderId = orderId; }
// ...demais getters/setters seguem o mesmo padrão
}
Buyer e Item são classes internas estáticas separadas. As regras de mapeamento, uma a uma:
| Forma no JSON | Tipo Java gerado | Observações |
|---|---|---|
"name": "王五" |
String |
Strings sempre viram String |
"qty": 2 |
Double |
Todo número vira Double, veja a seção 2 |
"vip": true |
Boolean |
Tipo wrapper, null permitido |
"buyer": {...} |
Buyer(classe interna estática) |
Objetos aninhados viram classe própria |
"items": [...] |
List<Item> |
Tipo do elemento vem do primeiro elemento |
order_id |
orderId |
snake_case convertido automaticamente para camelCase |
Nomes de classes recebem PascalCase (Root), campos recebem camelCase (orderId); colisões com palavras-chave Java (class, new, int) são resolvidas automaticamente com um sufixo. Existem três opções: packageName, className e useLombok, que alterna entre @Data e getters/setters escritos à mão. Com Lombok, todo o bloco de boilerplate colapsa em poucas anotações.
Duas fronteiras desse esqueleto valem ser conhecidas de antemão. Primeira: ele não gera nenhuma anotação Jackson/Gson — nem @JsonProperty, nem @JsonIgnoreProperties, nem @JsonFormat, porque anotações decidem como a desserialização executa, e uma amostra não revela a sua política de execução. Segunda: não há conversão reversa — derivar JSON de uma classe Java está fora do escopo, o mesmo desenho unidirecional do caminho JSON para TypeScript.
2. Correção número um: Double não é tipo para dinheiro
O gerador mapeia "price": 19.99 para Double. Compila, atribui — e ainda assim é o tipo errado. Ponto flutuante binário não consegue representar 0.1, logo tampouco a maioria das frações decimais:
System.out.println(19.99); // 19.99 — parece inofensivo
double a = 0.1 + 0.2;
System.out.println(a); // 0.30000000000000004
System.out.println(a == 0.3); // false
Imprimir 19.99 sozinho parece limpo porque o Java aplica embelezamento de representação mais curta; o erro aparece no momento em que o valor entra em aritmética. Os três textos de engenharia em inglês concordam: preços, valores e taxas devem ser BigDecimal, construído a partir de string — new BigDecimal("19.99"), não new BigDecimal(19.99), que carrega a imprecisão do double para dentro.
A correção mecânica é uma passada após a geração: troque Double por BigDecimal apenas nos campos monetários. Uma armadilha aparente na mesma raiz: inteiros acima de 2^53 (números de pedido, IDs snowflake) que passam por Double perdem silenciosamente os últimos dígitos — um número de 20 dígitos que perde os três últimos é um resultado medido e reproduzível, e o mecanismo é o mesmo descrito em por que o JSON perde precisão em inteiros grandes. Mapeie esses campos para String ou Long e deixe os dígitos viajarem como texto.
| Significado real do campo | O gerador dá | Trocar por |
|---|---|---|
| Dinheiro, taxas | Double |
BigDecimal(construtor string) |
| Números de pedido, IDs(podem passar de 2^53) | Double |
String ou Long |
| Quantidades, números de página | Double |
Integer / Long |
| Flutuantes científicos de verdade | Double |
manter |
3. Correção número dois: nomes que não batem, fixados com @JsonProperty
Converter order_id em orderId é um palpite bem-intencionado: o gerador assume que você quer camelCase no lado Java. Jackson, porém, casa as chaves do JSON com os nomes dos campos literalmente por padrão. Quando não batem, nada quebra — orderId simplesmente fica null. Duas correções:
Por campo — adicione @JsonProperty, uma anotação fixa um mapeamento:
public class Root {
@JsonProperty("order_id")
private Long orderId;
}
Global — troque a estratégia de nomes do ObjectMapper e nenhuma anotação por campo será necessária no projeto:
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
A escolha depende da superfície: uma API fixa, use nível de campo; todo o time fala com serviços em snake_case, use a estratégia global. Um fato negativo merece parágrafo próprio: um nome errado falha como null silencioso, não como exceção. A página da ferramenta jsonformater trata isso como a própria razão de geradores existirem: a cópia mecânica é exatamente onde essa taxa de erro atinge o pico. Depois de adicionar as anotações, escreva ao menos um teste de asserção de que os campos-chave não são null.
4. Correção número três: apagamento de genéricos — nunca desserialize num List nu
O gerador entrega a declaração List<Item>, mas o que de fato preenche essa lista é o Jackson. Escrever assim no nível raiz cai direto na armadilha do apagamento de genéricos:
// Errado: os elementos são LinkedHashMap em tempo de execução
List<Item> items = mapper.readValue(json, List.class);
// Correto: TypeReference preserva o parâmetro genérico
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
readValue(json, List.class) compila e executa, mas os genéricos do Java só existem em tempo de compilação. Em execução, o Jackson vê apenas List, então os elementos caem como LinkedHashMap. O erro não dispara de imediato — explode no primeiro item.getSku() em outro lugar, lançando uma ClassCastException longe do local do erro. Três formas corretas, por cenário:
// 1. O nível raiz é o array
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
// 2. Um campo de um objeto raiz
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});
// 3. Construção explícita de JavaType
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);
Do lado do Gson, o equivalente é TypeToken. Os três guias listam isso como item próprio, porque falha tarde e reporta longe.
5. Correção número quatro: campos desconhecidos — não deixe a evolução da API quebrar a desserialização
Uma API que adiciona um campo é a evolução mais comum possível. Mas o comportamento padrão do Jackson é lançar UnrecognizedPropertyException em campos desconhecidos — o provedor adiciona um campo e o seu serviço começa a errar. Desligue em dois níveis:
// Nível de classe: tolerar desconhecidos só nesta classe
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }
// Global: todo o mapper tolera
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Depois de desligar, surge um problema espelho: a tolerância também silencia erros de digitação nos nomes de campos, abrindo por completo o modo de "null silencioso" da seção anterior. O meio-termo: produção com tolerância desligada, suite de testes com tolerância ligada, para erros de digitação aparecerem durante os testes. O Gson ignora campos desconhecidos por padrão — por isso parece sem esforço em integrações rápidas, e o preço desse conforto é nunca saber o que o provedor adicionou.
6. Correção número cinco: datas e o construtor sem argumentos
O gerador mapeia created_at para String, o que na verdade é o padrão mais seguro: em JSON, uma data é só uma string, e o formato no fio pode ser ISO-8601, timestamp Unix ou algo personalizado — a amostra não diz qual você quer. Quando precisar de um tipo de data de verdade, adicione duas linhas:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Emitir ISO-8601 na serialização em vez de arrays de timestamps
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
O construtor sem argumentos é a outra checagem obrigatória: o Jackson desserializa criando o objeto por meio dele e depois ajustando campo a campo. Escreva um construtor parametrizado e o construtor padrão desaparece — a desserialização falha imediatamente. Com Lombok, adicione @NoArgsConstructor; com um record do Java 17, delegue tudo ao suporte a records do Jackson 2.12+, que casa pelos parâmetros do construtor, e a exigência desaparece por completo.
7. Dois pontos de produção: reutilizar o ObjectMapper e validar entradas
O gerador entrega classes; dois pontos de arquitetura permanecem em execução. Primeiro: um ObjectMapper é caro de criar e thread-safe — compartilhe uma instância na aplicação inteira. Chamar new ObjectMapper() dentro de um método é o antipadrão de desempenho mais comum, e a instância que o Spring Boot injeta por padrão existe justamente por isso. Estratégia de nomes, JavaTimeModule e política de campos desconhecidos pertencem a essa única instância, não a pontos de chamada espalhados.
Segundo: a desserialização bem-sucedida diz que os tipos casaram, não que os valores são legais. Preço negativo, quantidade zero, e-mail deformado — Jackson não se importa. A validação de entrada pertence ao Bean Validation (hibernate-validator), adicionada como anotações no esqueleto gerado:
public class CreateOrderRequest {
@NotNull
@DecimalMin(value = "0.01")
private BigDecimal price;
@Min(1)
private Integer qty;
@Email
private String contact;
}
No Spring, @Valid intercepta a requisição antes de alcançar a lógica de negócio. Essa camada é algo que um gerador jamais fornecerá — ele pode ler como o campo se parece na amostra, mas não qual restrição de negócio ele carrega.
8. Onde termina a geração e começa o trabalho manual
Quando gerar e quando escrever à mão — a linha é nítida:
- Use o gerador: respostas com muitos campos, integrações pontuais com terceiros, protótipos rápidos. Ele elimina exatamente uma classe de erros — falhas de cópia mecânica — e entrega em segundos um esqueleto alinhado em nomes e tipos.
- Escrever à mão ou retrabalhar: dinheiro e inteiros longos precisam de mudança de tipo, a estratégia de nomes precisa de decisão global, objetos centrais de domínio precisam de anotações de validação. São decisões de negócio que uma amostra não pode tomar.
- Pré-condição comum: confirme primeiro que o JSON de origem é válido. O gerador produz classes de bom grado a partir de entrada quebrada e deixa a armadilha para a desserialização; passe-o pela nossa página de validação antes de colar. Dez segundos, de qualquer forma.
O gerador cobre "de zero ao esqueleto". As cinco correções deste artigo cobrem "do esqueleto à produção". A ordem não pode ser invertida, e nenhum passo pode ser pulado.
FAQ
As classes geradas podem ser usadas direto no Spring Boot?
Na compilação, sim. Antes da execução, valem três acréscimos: campos monetários para BigDecimal, uma estratégia de nomes ou @JsonProperty para APIs em snake_case, e @JsonIgnoreProperties(ignoreUnknown = true) para sobreviver à evolução da API. Anotações de Bean Validation seguem suas regras de negócio.
Por que o gerador mapeia todo número para Double e não long ou BigDecimal?
Uma amostra distingue "inteiro contra decimal", não o significado de negócio. Um 2 pode ser quantidade ou número; um número de pedido de 20 dígitos e ¥19.99 são apenas números no JSON. Deixar as decisões semânticas para o usuário é um limite razoável para um gerador direto — o preço é a tabela de substituições da seção 2, que você mesmo percorre.
@Data do Lombok ou getters/setters escritos à mão — qual escolher?
Se o time já usa Lombok, tome @Data para o mínimo de boilerplate. Se preferir evitar um plugin de compilação ou conflitos com outros processadores de anotações, escreva à mão. Para o Jackson é indiferente: ele olha getters/setters ou visibilidade de campos.
Objetos aninhados viram classes internas estáticas — isso importa?
Para o Jackson não; uma classe interna estática é instanciável de forma independente. Para manutenção de longo prazo, a maioria dos times as extrai como classes de nível superior num pacote — para reuso e testes isolados. A estrutura de classes internas do gerador é feita para inspeção rápida, não para uma base de código viva.
Um campo List lança ClassCastException na desserialização — como localizar?
Primeiro verifique se a desserialização usou um List.class nu: os elementos na verdade são LinkedHashMap e o erro explode ao ler um campo. Troque por TypeReference. Se ainda falhar, cole a exceção na nossa página de busca por mensagem de erro e siga a entrada do seu caso.