Skip to Content
03 ImplementationObjectMapper Avancé

ObjectMapper Avancé

Guide avancé pour configurer et utiliser ObjectMapper de Jackson dans Micronaut : configuration custom, pretty-printing, et gestion des données JSON arbitraires.

Ce guide complète DTOs et Mapping en explorant les cas d’usage avancés de ObjectMapper.


Créer un ObjectMapper Custom

Par défaut, Micronaut fournit un ObjectMapper configuré. Mais vous pouvez en créer des personnalisés pour des usages spécifiques.

Créer une Factory

// infrastructure/config/ObjectMapperFactory.java package org.smoka.infrastructure.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; import jakarta.inject.Named; import jakarta.inject.Singleton; @Factory public class ObjectMapperFactory { /** * ObjectMapper pour les logs (pretty-print activé). */ @Bean @Named("logging") @Singleton public ObjectMapper loggingObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); // Pretty-print mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } /** * ObjectMapper compact pour les API externes. */ @Bean @Named("compact") @Singleton public ObjectMapper compactObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.disable(SerializationFeature.INDENT_OUTPUT); // Compact return mapper; } }

Injecter dans vos Composants

Injection par Défaut

@Controller("/api/trends") public class TrendController { private final ObjectMapper objectMapper; @Inject public TrendController(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Get public TrendResponseDto getTrends(@QueryValue String keyword) { TrendResponseDto response = // ... logique // Logger avec pretty-print temporaire try { String json = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(response); logger.info("Response:\n{}", json); } catch (Exception e) { logger.error("Failed to serialize response", e); } return response; } }

Créer une Classe Utilitaire

Pour éviter la répétition, créez un helper de logging JSON.

// infrastructure/util/JsonLogger.java package org.smoka.infrastructure.util; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; import org.slf4j.Logger; @Singleton public class JsonLogger { private final ObjectMapper loggingMapper; @Inject public JsonLogger(@Named("logging") ObjectMapper loggingMapper) { this.loggingMapper = loggingMapper; } public void logInfo(Logger logger, String message, Object object) { try { String json = loggingMapper.writeValueAsString(object); logger.info("{}\n{}", message, json); } catch (Exception e) { logger.error("Failed to serialize object for logging", e); } } public void logDebug(Logger logger, String message, Object object) { try { String json = loggingMapper.writeValueAsString(object); logger.debug("{}\n{}", message, json); } catch (Exception e) { logger.error("Failed to serialize object for logging", e); } } }

Utilisation :

@Inject private JsonLogger jsonLogger; @Get public TrendResponseDto getTrends(@QueryValue String keyword) { TrendResponseDto response = // ... jsonLogger.logInfo(logger, "Trend response:", response); return response; }

Données JSON Arbitraires

Comment gérer des données JSON non structurées (sessions Redis, metadata, cache) tout en respectant l’architecture hexagonale ?

Problème : Le domain ne doit PAS dépendre de Jackson (JsonNode = Jackson), mais on veut stocker du JSON flexible.

Le Problème

JsonNode dans le Domain (INTERDIT)

// domain/model/session/Session.java @Value public class Session { String sessionId; JsonNode metadata; // ❌ Jackson dans le domain ! }

Problèmes :

  • Le domain dépend de Jackson
  • Viole l’architecture hexagonale
  • Difficile à tester sans Jackson

Exemple Complet : Session Redis

Voyons comment gérer des sessions avec metadata flexible en respectant l’architecture hexagonale.

Architecture

      • SessionRedisDto.java
      • SessionRepositoryAdapter.java

Flux de Données

Client REST (envoie JSON) SessionController ↓ SessionDto { metadata: Map<String, Object> } ↓ Conversion DTO → Domain CreateSessionUseCase ↓ Session { metadata: Map<String, Object> } SessionRepository (interface) SessionRepositoryAdapter ↓ fromDomain() : Session → SessionRedisDto ↓ objectMapper.valueToTree() : Map → JsonNode Redis (stockage avec JsonNode)

Implémentation

Domain Layer (PUR)

// domain/model/session/Session.java package org.smoka.domain.model.session; import lombok.Value; import java.time.Instant; import java.util.Map; @Value public class Session { String sessionId; String userId; Map<String, Object> metadata; // ✅ Pas de Jackson Instant createdAt; }
// domain/port/output/SessionRepository.java package org.smoka.domain.port.output; import org.smoka.domain.model.session.Session; import java.util.Optional; public interface SessionRepository { void save(Session session); Optional<Session> findById(String sessionId); void deleteById(String sessionId); }

Application Layer

// application/port/input/dto/SessionDto.java package org.smoka.application.port.input.dto; import lombok.Data; import java.time.Instant; import java.util.Map; @Data public class SessionDto { private String sessionId; private String userId; private Map<String, Object> metadata; // ← Reçu depuis le client REST private Instant createdAt; }
// application/port/input/SessionController.java @Controller("/api/sessions") public class SessionController { private final CreateSessionUseCase createSessionUseCase; @Post public SessionDto createSession(@Body SessionDto dto) { // DTO → Domain Session session = new Session( dto.getSessionId(), dto.getUserId(), dto.getMetadata(), // Map<String, Object> dto.getCreatedAt() ); // Exécuter le use case Session created = createSessionUseCase.execute(session); // Domain → DTO SessionDto response = new SessionDto(); response.setSessionId(created.getSessionId()); response.setUserId(created.getUserId()); response.setMetadata(created.getMetadata()); response.setCreatedAt(created.getCreatedAt()); return response; } }

Infrastructure Layer

// infrastructure/dto/SessionRedisDto.java package org.smoka.infrastructure.dto; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class SessionRedisDto { @JsonProperty("sessionId") private String sessionId; @JsonProperty("userId") private String userId; @JsonProperty("metadata") private JsonNode metadata; // ✅ Jackson dans l'infrastructure @JsonProperty("createdAt") private String createdAt; }

Adapter avec Conversions

// infrastructure/adapter/SessionRepositoryAdapter.java package org.smoka.infrastructure.adapter; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.smoka.domain.model.session.Session; import org.smoka.domain.port.output.SessionRepository; import org.smoka.infrastructure.dto.SessionRedisDto; import java.time.Instant; import java.util.Map; import java.util.Optional; @Singleton public class SessionRepositoryAdapter implements SessionRepository { private final ObjectMapper objectMapper; @Inject public SessionRepositoryAdapter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public void save(Session session) { // Domain → DTO SessionRedisDto dto = fromDomain(session); // Sérialiser et stocker dans Redis try { String json = objectMapper.writeValueAsString(dto); // redisClient.set(session.getSessionId(), json); } catch (Exception e) { throw new RuntimeException("Failed to save session", e); } } @Override public Optional<Session> findById(String sessionId) { try { // String json = redisClient.get(sessionId); // if (json == null) return Optional.empty(); // Désérialiser depuis Redis // SessionRedisDto dto = objectMapper.readValue(json, SessionRedisDto.class); // DTO → Domain // return Optional.of(toDomain(dto)); return Optional.empty(); } catch (Exception e) { throw new RuntimeException("Failed to find session", e); } } @Override public void deleteById(String sessionId) { // redisClient.delete(sessionId); } /** * Convertit Session (domain) en SessionRedisDto (infrastructure). * Conversion : Map<String, Object> → JsonNode */ private SessionRedisDto fromDomain(Session session) { SessionRedisDto dto = new SessionRedisDto(); dto.setSessionId(session.getSessionId()); dto.setUserId(session.getUserId()); dto.setCreatedAt(session.getCreatedAt().toString()); // ✅ Map<String, Object> → JsonNode dto.setMetadata(objectMapper.valueToTree(session.getMetadata())); return dto; } /** * Convertit SessionRedisDto (infrastructure) en Session (domain). * Conversion : JsonNode → Map<String, Object> */ private Session toDomain(SessionRedisDto dto) { // ✅ JsonNode → Map<String, Object> Map<String, Object> metadata = objectMapper.convertValue( dto.getMetadata(), new TypeReference<Map<String, Object>>() {} ); return new Session( dto.getSessionId(), dto.getUserId(), metadata, Instant.parse(dto.getCreatedAt()) ); } }

Méthodes ObjectMapper Clés

Convertir Map vers JsonNode

Map<String, Object> map = Map.of( "key", "value", "count", 42, "active", true ); JsonNode jsonNode = objectMapper.valueToTree(map);

Cas d’usage : Convertir les metadata du domain vers le DTO infrastructure.


Autres Approches

Approche Map (Recommandée)

Domain :

@Value public class Session { String sessionId; Map<String, Object> metadata; // ✅ Flexible et pur }

Avantages :

  • ✅ Domain indépendant de Jackson
  • ✅ Haute flexibilité
  • ✅ Facile à manipuler
  • ✅ Type-safety limitée mais acceptable

Inconvénients :

  • ⚠️ Pas de validation de structure
  • ⚠️ Cast nécessaire pour typer les valeurs

Quand l’utiliser :

  • Metadata dynamiques
  • Configuration utilisateur
  • Payload flexible

Comparaison

ApprocheFlexibilitéType-SafetyComplexitéIndépendance Domain
Map<String, Object>✅ Haute⚠️ Limitée⭐ Simple✅ Totale
String (JSON brut)✅ Haute❌ Aucune⭐ Très simple✅ Totale
Type dédié❌ Faible✅ Forte⭐⭐⭐ Complexe✅ Totale

Recommandation : Utilisez Map&lt;String, Object&gt; pour un bon compromis entre flexibilité et pureté du domain.


Cas d’Usage Pratiques

Cette approche avec Map&lt;String, Object&gt; est idéale pour :

Sessions Redis

@Value public class Session { String sessionId; String userId; Map<String, Object> metadata; // Theme, langue, préférences Instant createdAt; }

Exemple metadata :

{ "theme": "dark", "language": "fr", "notifications": true, "lastActivity": "2025-10-18T16:30:00Z" }

Configuration via application.yml

Configurez l’ObjectMapper global directement dans application.yml.

jackson: serialization: indentOutput: true # Pretty-print writeDatesAsTimestamps: false # Dates en ISO-8601 failOnEmptyBeans: false # Ne pas échouer si objet vide deserialization: failOnUnknownProperties: false # Ignorer propriétés inconnues acceptSingleValueAsArray: true # "value" → ["value"] module-scan: true # Scanner modules Jackson auto defaultPropertyInclusion: non_null # N'inclure que les non-null

Cette configuration s’applique à tous les ObjectMapper créés par Micronaut (sauf ceux créés manuellement).


Checklist Architecture Hexagonale

Pour vérifier que vous utilisez ObjectMapper correctement :

  • Le domain n’importe AUCUNE classe Jackson
  • Les entités du domain utilisent Map&lt;String, Object&gt; ou String (pas JsonNode)
  • Les DTOs infrastructure sont dans infrastructure/dto/
  • Les DTOs application sont dans application/port/input/dto/
  • La conversion DTO infrastructure ↔ Domain est dans l’adaptateur
  • L’adaptateur utilise objectMapper.valueToTree() pour Map → JsonNode
  • L’adaptateur utilise objectMapper.convertValue() pour JsonNode → Map
  • Le domain peut être testé sans Jackson
  • jackson.module-scan: true dans application.yml

Résumé

Utilisez des ObjectMapper custom pour des besoins spécifiques (logs, API externes). Pour les données JSON arbitraires, préférez Map<String, Object> dans le domain au lieu de JsonNode.

ConceptEmplacementUtilise Jackson ?Format Données
Domaindomain/model/❌ NONMap&lt;String, Object&gt;
DTO Applicationapplication/port/input/dto/❌ NONMap&lt;String, Object&gt;
DTO Infrastructureinfrastructure/dto/✅ OUIJsonNode
Adapterinfrastructure/adapter/✅ OUIConversions Map ↔ JsonNode

Règle d’or : Jackson reste confiné dans infrastructure/. Le domain est pur et indépendant.


Prochaines étapes

Vous avez maintenant terminé la section Implementation ! Passez aux bonnes pratiques pour écrire du code production-ready.

Last updated on