Traitement personnalisé de la sérialisation JSON pour des types spécifiques dans Spring MVC

Dans les applications Spring MVC, la gestion de la conversion entre objets Java et format JSON pour des types particuliers, comme LocalDateTime, des énumérations sur mesure ou des données sensibles, requiert une approche personnalisée. Cette personnalisation s'effectue grâce aux mécanismes d'extension de Jackson, notmament les classes JsonSerializer et JsonDeserializer, couplés aux configurations de Spring pour une application globale.

Fonctionnement fondamental

Le framework Jackson repose sur une architecture modulaire pour étendre ses capacités de (dé)sérialisation :

  • JsonSerializer<T> : définit la logique pour convertir une instance de T en représentation JSON.
  • JsonDeserializer<T> : définit la logique pour reconstruire une instance de T à partir de données JSON.
  • SimpleModule : permet d'enregistrer les sérialiseurs et désérialiseurs personnalisés auprès de l'ObjectMapper.
  • Intégration Spring : via la définition d'un Bean ObjectMapper ou l'implémentation de WebMvcConfigurer, on assure que les transformations personnalisées s'appliquent à l'ensemble de l'application.

Cas d'usage typiques

  1. Standardiser le format d'affichage de LocalDateTime selon un motif comme "dd/MM/yyyy HH:mm".
  2. Appliquer un masquage partiel sur des numéros de téléphone (ex. : 06** **56 78).
  3. Personnaliser la représentation JSON d'une énumération, par exemple en utilisant un code numérique au lieu de son nom.
  4. Convertir des chaînes horodatées provenant du client en instances de Date côté serveur.

Exemple complet : gestion sur mesure de LocalDateTime

Définition du sérialiseur (Java → JSON)

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormateurLocalDateTime extends JsonSerializer<LocalDateTime> {
    private static final DateTimeFormatter motif = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

    @Override
    public void serialize(LocalDateTime dateHeure, JsonGenerator gen, SerializerProvider prov) throws IOException {
        if (dateHeure == null) {
            gen.writeNull();
        } else {
            gen.writeString(motif.format(dateHeure));
        }
    }
}

Définition du désérialiseur (JSON → Java)

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ConvertisseurLocalDateTime extends JsonDeserializer<LocalDateTime> {
    private static final DateTimeFormatter motif = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

    @Override
    public LocalDateTime deserialize(JsonParser parseur, ContexteDesérialisation ctxt) throws IOException {
        String texte = parseur.getText();
        if (texte == null || texte.isEmpty()) {
            return null;
        }
        return LocalDateTime.parse(texte, motif);
    }
}

Enregistrement via Spring Boot

La méthode recommandée consiste à définir un Bean personnalisé pour l'ObjectMapper :

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfigurationJackson {
    @Bean
    public ObjectMapper mapperPersonnalise() {
        ObjectMapper mapper = new ObjectMapper();
        // Support des types temporels JSR-310 (recommandé)
        mapper.registerModule(new JavaTimeModule());
        // Module pour nos personnalisations
        SimpleModule module = new SimpleModule();
        module.addSerializer(LocalDateTime.class, new FormateurLocalDateTime());
        module.addDeserializer(LocalDateTime.class, new ConvertisseurLocalDateTime());
        mapper.registerModule(module);
        // Désactiver le format timestamp pour éviter les conflits
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper;
    }
}

Validation avec un contrôleur Spring MVC

import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api")
public class ControleurDateHeure {
    @GetMapping("/date-courante")
    public ReponseDate obtenirDate() {
        return new ReponseDate(LocalDateTime.now());
    }

    @PostMapping("/date-courante")
    public String definirDate(@RequestBody ReponseDate demande) {
        System.out.println("Date reçue : " + demande.getHorodatage());
        return "Succès";
    }
}

class ReponseDate {
    private LocalDateTime horodatage;

    // Constructeur, getters et setters
    public ReponseDate(LocalDateTime horodatage) {
        this.horodatage = horodatage;
    }
    public LocalDateTime getHorodatage() { return horodatage; }
    public void setHorodatage(LocalDateTime horodatage) { this.horodatage = horodatage; }
}

Les requêtes et réponses échangent alors des dates au format "dd/MM/yyyy HH:mm", assurant une cohérence côté client et serveur.

Application à d'autres types : maqsuage de numéros de téléphone

Pour un masquage partiel, on peut créer un sérialiseur dédié, appliqué localement via une annotation :

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;

public class MasqueurTelephone extends JsonSerializer<String> {
    @Override
    public void serialize(String numero, JsonGenerator gen, SerializerProvider prov) throws IOException {
        if (numero != null && numero.length() == 10) {
            String masque = numero.substring(0, 2) + "** **" + numero.substring(6);
            gen.writeString(masque);
        } else {
            gen.writeString(numero);
        }
    }
}

// Utilisation dans une entité
public class Utilisateur {
    private String nom;

    @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = MasqueurTelephone.class)
    private String telephone;

    // Getters et setters
}

Une application globale nécessiterait des mécanismes plus avancés, comme un BeanSerializerModifier basé sur des règles de nommage ou des annotations personnalisées.

Considérations importantes

  • Éviter les enregistrements multiples : une double configuration (ex. : JavaTimeModule et sérialiseur manuel pour LocalDateTime) peut causer des conflits.
  • Les sérialiseurs et désérialiseurs doivent être sans état pour garantir la sécurité des threads.
  • Les exceptions lancées dans deserialize() sont converties par Spring en réponses HTTP 400.
  • Lorsque l'on fournit un Bean ObjectMapper personnalisé, Spring Boot l'utilise automatiquement dans le MappingJackson2HttpMessageConverter.

Récapitulatif des étapes

  1. Implémenter JsonSerializer<T> et/ou JsonDeserializer<T> pour le type cible.
  2. Enregistrer ces classes dans un SimpleModule, puis injecter ce module dans l'ObjectMapper.
  3. Définir un Bean Spring pour l'ObjectMapper personnalisé.
  4. Optionnellement, utiliser les annotations @JsonSerialize et @JsonDeserialize pour des cas spécifiques.

Étiquettes: Jackson Spring MVC Java JsonSerializer JsonDeserializer

Publié le 10 juillet à 22h39