JSON Toolkit

JSON vers classe Java : le générateur fournit un squelette, la production exige cinq ajouts

L’essentiel : Un examen concret de ce qu'un générateur JSON → Java produit vraiment——les nombres deviennent Double, les objets imbriqués deviennent des classes internes statiques, et aucune annotation Jackson n'est émise. Précision monétaire, renommage des champs, effacement des génériques, champs inconnus et dates : cinq choses à ajouter soi-même, avec du code prêt à copier.

Intermédiaire~10 min de lecturePublié le 2026-11-04Mis à jour le 2026-11-04
Interface d'un outil convertissant du JSON en classes POJO Java

En bref : la classe générée compile immédiatement, mais n'est pas prête pour la production. Les valeurs par défaut mesurées de notre générateur : number→Double, objets imbriqués→classes internes statiques, aucune annotation Jackson. Précision monétaire, renommage des champs, effacement des génériques, champs inconnus et dates — cinq choses qu'il ne fera jamais à votre place.

Transformer une réponse d'API en classes Java est un travail mécanique : lire le JSON, copier les champs, les envelopper de getters et setters. Le prix du travail mécanique, c'est le couteau invisible : une erreur d'une seule lettre dans un nom de propriété ne casse pas la compilation. Le champ arrive silencieusement null, et vous le découvrez dans un log de production. La génération de structure de classe existe pour aligner noms et types dès le premier jour. Mais le générateur ne peut aligner que ce qui est visible dans l'échantillon.

1. Lancez d'abord le générateur : que donne-t-il réellement

Collez une réponse représentative dans notre outil JSON vers 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"
}

Le squelette généré (options par défaut, useLombok désactivé) :

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; }
    // ...les autres getters/setters suivent le même modèle
}

Buyer et Item sont des classes internes statiques distinctes. Les règles de mappage, une par une :

Forme dans le JSON Type Java généré Remarques
"name": "王五" String Les chaînes deviennent toujours String
"qty": 2 Double Tous les nombres deviennent Double, voir section 2
"vip": true Boolean Type boîte, null autorisé
"buyer": {...} Buyer(classe interne statique) Les objets imbriqués deviennent leur propre classe
"items": [...] List<Item> Type d'élément pris dans le premier élément
order_id orderId snake_case converti automatiquement en camelCase

Les noms de classes reçoivent du PascalCase (Root), les champs du camelCase (orderId) ; les collisions avec les mots-clés Java (class, new, int) sont résolues automatiquement par un suffixe. Trois options existent : packageName, className et useLombok, qui bascule entre @Data et des getters/setters écrits à la main. Avec Lombok, tout le bloc de code passe-partout se réduit à quelques annotations.

Deux limites de ce squelette méritent d'être connues à l'avance. D'abord, il ne génère aucune annotation Jackson/Gson — ni @JsonProperty, ni @JsonIgnoreProperties, ni @JsonFormat, car les annotations décident de la façon dont la désérialisation s'exécute, et un échantillon ne révèle pas votre politique d'exécution. Ensuite, pas de conversion inverse : dériver du JSON depuis une classe Java est hors de portée, même conception unidirectionnelle que le chemin JSON vers TypeScript.

2. Correction numéro un : Double n'est pas un type pour l'argent

Le générateur mappe "price": 19.99 en Double. Ça compile, ça s'affecte, et c'est quand même le mauvais type. Le flottant binaire ne peut pas représenter 0.1, donc pas davantage la plupart des fractions décimales :

System.out.println(19.99);          // 19.99 — semble correct
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

Imprimer 19.99 seul paraît propre parce que Java applique un embellissement en représentation la plus courte ; l'erreur surgit dès que la valeur entre dans un calcul. Les trois écrits d'ingénierie anglophones sont unanimes : prix, montants et taux doivent être BigDecimal, construits à partir d'une chaîne — new BigDecimal("19.99"), pas new BigDecimal(19.99), qui embarque la précision erronée du double telle quelle.

Le correctif mécanique est une passe après génération : remplacez Double par BigDecimal uniquement pour les champs monétaires. Piège apparenté de même racine : les entiers au-delà de 2^53 (numéros de commande, identifiants snowflake) qui passent par Double perdent silencieusement leurs derniers chiffres — un nombre à 20 chiffres qui en perd trois est un résultat mesuré et reproductible, et le mécanisme est le même que celui décrit dans pourquoi JSON perd la précision des grands entiers. Mappez ces champs en String ou Long et laissez les chiffres voyager en texte.

Sens réel du champ Le générateur donne Remplacer par
Argent, taux Double BigDecimal(constructeur chaîne)
Numéros de commande, ID(peuvent dépasser 2^53) Double String ou Long
Quantités, numéros de page Double Integer / Long
Vrais flottants scientifiques Double conserver

3. Correction numéro deux : les noms qui ne correspondent pas, épinglés par @JsonProperty

Convertir order_id en orderId est une hypothèse bienveillante : le générateur suppose que vous voulez du camelCase côté Java. Jackson, lui, fait correspondre les clés JSON aux noms de champs littéralement par défaut. Quand ils ne correspondent pas, rien ne casse — orderId reste simplement null. Deux correctifs :

Par champ — ajoutez @JsonProperty, une annotation épinglera un mappage :

public class Root {
    @JsonProperty("order_id")
    private Long orderId;
}

Global — changez la stratégie de nommage de l'ObjectMapper et plus aucune annotation par champ n'est nécessaire dans le projet :

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

Le choix dépend de la surface : un API fixe, utilisez le niveau champ ; toute l'équipe parle à des services en snake_case, utilisez la stratégie globale. Un fait négatif mérite son propre paragraphe : un nom erroné échoue en null silencieux, pas en exception. La page de l'outil jsonformater en fait la raison d'être même des générateurs : la copie mécanique est précisément là où ce taux d'erreur culmine. Après avoir ajouté les annotations, écrivez au moins un test d'assertion que les champs clés ne sont pas null.

4. Correction numéro trois : l'effacement des génériques — ne désérialisez jamais dans un List nu

Le générateur vous remet la déclaration List<Item>, mais ce qui remplit réellement cette liste, c'est Jackson. Écrire ceci au niveau racine tombe directement dans le piège de l'effacement des génériques :

// Faux : les éléments sont des LinkedHashMap à l'exécution
List<Item> items = mapper.readValue(json, List.class);

// Correct : TypeReference préserve le paramètre générique
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) compile et s'exécute, mais les génériques Java n'existent qu'à la compilation. À l'exécution, Jackson ne voit qu'un List, donc les éléments atterrissent en LinkedHashMap. L'erreur ne se déclenche pas immédiatement — elle explose au premier item.getSku() ailleurs, lançant une ClassCastException loin de l'endroit de la faute. Trois formes correctes, par scénario :

// 1. Le niveau racine est le tableau
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. Un champ d'un objet racine
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

// 3. Construction explicite d'un JavaType
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);

Côté Gson, l'équivalent est TypeToken. Les trois guides le listent comme un point à part entière, parce qu'il échoue tard et rapporte loin.

5. Correction numéro quatre : les champs inconnus — ne laissez pas l'évolution de l'API casser la désérialisation

L'ajout d'un champ par un API est l'évolution la plus ordinaire. Mais le comportement par défaut de Jackson est de lever UnrecognizedPropertyException sur les champs inconnus — le fournisseur ajoute un champ et votre service se met à erreurs. Désactivez-le à deux niveaux :

// Niveau classe : tolérer les inconnus seulement dans cette classe
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// Global : tout le mapper les tolère
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Une fois désactivé, un problème miroir apparaît : la tolérance réduit aussi au silence les fautes de frappe dans les noms de champs, ouvrant en grand le mode « null silencieux » de la section précédente. Le compromis : production avec tolérance désactivée, tests avec tolérance activée, pour faire remonter les coquilles pendant les tests. Gson ignore les champs inconnus par défaut — c'est pourquoi il semble si facile pour les intégrations rapides, et le prix de ce confort est de ne jamais savoir ce que le fournisseur a ajouté.

6. Correction numéro cinq : les dates et le constructeur sans argument

Le générateur mappe created_at en String, ce qui est en fait le choix par défaut le plus sûr : dans le JSON, une date n'est qu'une chaîne, et le format fil peut être ISO-8601, une estampe Unix ou quelque chose de personnalisé — l'échantillon ne dit pas lequel vous voulez. Quand un vrai type de date est nécessaire, ajoutez deux lignes :

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Émettre de l'ISO-8601 à la sérialisation au lieu de tableaux d'estampes
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

Le constructeur sans argument est l'autre vérification obligatoire : Jackson désérialise en créant l'objet par son intermédiaire, puis en fixant les champs un à un. Écrivez un constructeur paramétré et celui par défaut disparaît — la désérialisation échoue immédiatement. Avec Lombok, ajoutez @NoArgsConstructor ; avec un record Java 17, confiez tout au support des records de Jackson 2.12+, qui apparie par paramètres de constructeur, et l'exigence disparaît entièrement.

7. Deux points de production : réutiliser l'ObjectMapper et valider les entrées

Le générateur donne des classes ; deux points d'architecture restent à l'exécution. D'abord, un ObjectMapper est coûteux à créer et thread-safe — partagez une instance dans toute l'application. Appeler new ObjectMapper() dans une méthode est l'antipattern de performance le plus courant, et l'instance injectée par défaut par Spring Boot existe précisément pour cela. Stratégie de nommage, JavaTimeModule et politique des champs inconnus appartiennent à cette seule instance, pas à des sites d'appel éparpillés.

Ensuite, une désérialisation réussie dit que les types ont correspondu, pas que les valeurs sont légales. Un prix négatif, une quantité nulle, un e-mail mal formé — Jackson s'en moque. La validation d'entrée appartient à Bean Validation (hibernate-validator), ajoutée en annotations sur le squelette généré :

public class CreateOrderRequest {
    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

Dans Spring, @Valid intercepte la requête avant la logique métier. Cette couche est celle qu'un générateur ne fournira jamais — il peut lire à quoi ressemble un champ dans l'échantillon, mais pas quelle contrainte métier il porte.

8. Où finit la génération et commence l'écriture manuelle

Quand générer et quand écrire à la main : la ligne est claire :

  • Utilisez le générateur : réponses à nombreux champs, intégrations tierces ponctuelles, prototypes rapides. Il élimine exactement une classe d'erreurs — les fautes de copie mécanique — et livre en quelques secondes un squelette aligné en noms et types.
  • Écrire à la main ou retoucher : argent et longs entiers à retyper, stratégie de nommage à décider globalement, objets de domaine clés à annoter pour la validation. Ce sont des décisions métier qu'un échantillon ne peut pas prendre.
  • Précondition commune : vérifiez d'abord que le JSON source est valide. Le générateur fabrique volontiers des classes à partir d'une entrée cassée et laisse le piège à la désérialisation ; passez-le par notre page de validation avant de coller. Dix secondes, dans tous les cas.

Le générateur couvre « de zéro au squelette ». Les cinq correctifs de cet article couvrent « du squelette à la production ». L'ordre ne peut être inversé, et aucune étape ne peut être sautée.

FAQ

Les classes générées peuvent-elles être utilisées directement dans Spring Boot ?

À la compilation, oui. Avant l'exécution, trois ajouts valent la peine : les champs monétaires en BigDecimal, une stratégie de nommage ou @JsonProperty pour les API snake_case, et @JsonIgnoreProperties(ignoreUnknown = true) pour survivre à l'évolution de l'API. Les annotations Bean Validation suivent vos règles métier.

Pourquoi le générateur mappe-t-il tous les nombres en Double plutôt qu'en long ou BigDecimal ?

Un échantillon distingue « entier contre décimal », pas le sens métier. Un 2 peut être une quantité ou un identifiant ; un numéro de commande à 20 chiffres et ¥19.99 ne sont que des nombres en JSON. Laisser les décisions sémantiques à l'utilisateur est une limite raisonnable pour un générateur direct ; le prix, c'est le tableau de remplacement de la section 2, à parcourir vous-même.

@Data de Lombok ou getters/setters écrits à la main — que choisir ?

Si l'équipe utilise déjà Lombok, prenez @Data pour le moins de passe-partout. Si vous préférez éviter un plugin de compilation ou des conflits avec d'autres processeurs d'annotations, écrivez à la main. Jackson s'en moque : il regarde les getters/setters ou la visibilité des champs.

Les objets imbriqués deviennent des classes internes statiques — est-ce grave ?

Pas pour Jackson ; une classe interne statique s'instancie indépendamment. Pour la maintenance longue, la plupart des équipes les extraient en classes de premier niveau dans un package — pour réutilisation et tests isolés. La structure en classes internes du générateur est faite pour l'inspection rapide, pas pour une base de code vivante.

Un champ List lance ClassCastException lors de la désérialisation — comment le localiser ?

Vérifiez d'abord si la désérialisation a utilisé un List.class nu : les éléments sont en réalité des LinkedHashMap et l'erreur explose à la lecture d'un champ. Passez à TypeReference. Si ça échoue encore, collez l'exception dans notre page de recherche par message d'erreur et suivez l'entrée de votre langage.