Skip to Content
03 ImplementationDTOs et Mapping avec Jackson

DTOs et Mapping avec Jackson

Guide complet sur l’utilisation des DTOs (Data Transfer Objects) et la conversion avec Jackson dans l’architecture hexagonale.

Règle d’or : Le domain ne doit JAMAIS dépendre de Jackson ou toute autre technologie de sérialisation.


Principe Fondamental

Dans l’architecture hexagonale, le domain doit rester pur et indépendant des frameworks.

Jackson dans le Domain

// domain/model/Country.java @Value public class Country { @JsonProperty("name") // ❌ Jackson dans le domain ! Name name; @JsonDeserialize(using = CustomDeserializer.class) // ❌ List<Translation> translations; }

Problème : Le domain dépend de Jackson → impossible de changer de librairie sans casser le domain.


Architecture avec DTOs

Structure clé :

  • domain/model = Entités PURES (pas de Jackson)
  • infrastructure/dto = DTOs avec annotations Jackson
  • infrastructure/adapter = Conversion DTO ↔ Domain

Flux de Données

Voyons comment les données traversent les couches.

API Externe renvoie du JSON

{ "name": { "common": "Lithuania", "official": "Republic of Lithuania" }, "translations": { "ara": { "official": "جمهورية ليتوانيا", "common": "ليتوانيا" }, "fra": { "official": "République de Lituanie", "common": "Lituanie" } } }

Jackson désérialise vers DTO (automatique)

// infrastructure/dto/CountryApiDto.java @Data @NoArgsConstructor public class CountryApiDto { @JsonProperty("name") private NameDto name; @JsonProperty("translations") private Map<String, TranslationDto> translations; }

Le HttpClient de Micronaut utilise Jackson automatiquement.

Adaptateur convertit DTO → Domain

// infrastructure/adapter/CountryRepositoryAdapter.java private Country toDomain(CountryApiDto dto) { Name name = new Name( dto.getName().getCommon(), dto.getName().getOfficial() ); List<Translation> translations = convertTranslations( dto.getTranslations() ); return new Country(name, translations); }

Domain pur utilisé par les Use Cases

// domain/model/countries/Country.java @Value public class Country { Name name; List<Translation> translations; // ← List (idiomatique) }

À chaque étape, le domain reste pur et indépendant de Jackson.


Conversion dans l’Adaptateur

L’adaptateur est responsable de la traduction entre le monde technique (DTOs) et le domain.

Exemple Complet

@Slf4j @Singleton public class CountryRepositoryAdapter implements CountryRepository { @Inject @Client(id = "countries", path = "/v3.1") HttpClient httpClient; @Override public List<Country> findAll() { try { // 1. Appeler l'API → Jackson désérialise automatiquement CountryApiDto[] countriesDto = httpClient.toBlocking() .retrieve("/all?fields=name,translations", CountryApiDto[].class); // 2. Convertir DTO → Domain return Stream.of(countriesDto) .map(this::toDomain) .collect(Collectors.toList()); } catch (Exception e) { log.error("Error retrieving countries", e); return List.of(); } } private Country toDomain(CountryApiDto dto) { // Conversion DTO (infrastructure) → Domain (pur) Name name = new Name( dto.getName().getCommon(), dto.getName().getOfficial() ); List<Translation> translations = convertTranslations( dto.getTranslations() ); return new Country(name, translations); } private List<Translation> convertTranslations( Map<String, TranslationDto> translationsMap ) { if (translationsMap == null) { return List.of(); } return translationsMap.entrySet().stream() .map(entry -> new Translation( entry.getKey(), // languageCode entry.getValue().getCommon(), entry.getValue().getOfficial() )) .collect(Collectors.toList()); } }

Où faire la conversion ?

  • ❌ MAUVAIS : Dans le DTO (créerait une dépendance DTO → Domain)
  • ✅ BON : Dans l’Adaptateur (responsabilité de traduction)

ObjectMapper dans Micronaut

Configuration par Défaut

Micronaut crée automatiquement un ObjectMapper configuré.

Dans application.yml :

jackson: serialization: indentOutput: true # Pretty-print JSON writeDatesAsTimestamps: false # Dates en ISO-8601 deserialization: failOnUnknownProperties: false # Ignorer propriétés inconnues module-scan: true # Scanner modules Jackson auto

Injection de l’ObjectMapper

@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 try { String json = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(response); logger.info("Response:\n{}", json); } catch (Exception e) { logger.error("Failed to serialize response", e); } return response; } }

ObjectMapper Custom

Créer un ObjectMapper dédié

Utile pour avoir différentes configurations (logs, API externes, etc.).

@Factory public class ObjectMapperFactory { @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; } @Bean @Named("compact") @Singleton public ObjectMapper compactObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.disable(SerializationFeature.INDENT_OUTPUT); // Compact return mapper; } }

Injection d’un ObjectMapper custom

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

Données JSON Arbitraires

Comment gérer des données JSON non structurées (Redis, sessions, metadata) sans violer l’architecture hexagonale ?

Le Problème

// ❌ MAUVAIS - JsonNode dans le domain @Value public class Session { String sessionId; JsonNode metadata; // ❌ Jackson dans le domain ! }

Solution : Map<String, Object>

Domain (Pur)

@Value public class Session { String sessionId; String userId; Map<String, Object> metadata; // ✅ Java pur Instant createdAt; }

Méthodes ObjectMapper Clés

// Map<String, Object> → JsonNode JsonNode jsonNode = objectMapper.valueToTree(map); // JsonNode → Map<String, Object> Map<String, Object> map = objectMapper.convertValue( jsonNode, new TypeReference<Map<String, Object>>() {} ); // String (JSON brut) → JsonNode JsonNode jsonNode = objectMapper.readTree(json); // JsonNode → String (JSON brut) String json = objectMapper.writeValueAsString(jsonNode);

Annotations Jackson Utiles

Pour vos DTOs dans infrastructure/dto/ :

@Data public class TrendResponseDto { @JsonProperty("keyword") private String keyword; @JsonProperty("interest_score") // Snake case dans le JSON private Integer interestScore; @JsonInclude(JsonInclude.Include.NON_NULL) // Ne pas inclure si null private List<RelatedTopic> relatedTopics; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime queriedAt; @JsonIgnore // Ne pas sérialiser ce champ private String internalData; }

Checklist Architecture Hexagonale

Pour vérifier que tu utilises correctement Jackson :

  • Le domain n’importe AUCUNE classe Jackson (@JsonProperty, JsonNode, etc.)
  • Les DTOs sont dans infrastructure/dto/
  • Les DTOs utilisent @Data + @NoArgsConstructor (Lombok)
  • Les DTOs mappent exactement la structure JSON de l’API externe
  • La conversion DTO → Domain est dans l’adaptateur (méthode toDomain)
  • Les DTOs ne connaissent PAS le domain (pas d’import org.smoka.domain.*)
  • Pour les données arbitraires, le domain utilise Map<String, Object> (pas JsonNode)
  • jackson.module-scan: true dans application.yml

Résumé

Les DTOs permettent de protéger le domain des dépendances techniques tout en facilitant la communication avec les APIs externes.

ConceptEmplacementUtilise Jackson ?Rôle
DTOinfrastructure/dto/✅ OuiMapper le JSON de l’API externe
Adaptateurinfrastructure/adapter/✅ Oui (automatique via HttpClient)Convertir DTO ↔ Domain
Domaindomain/model/❌ NONEntités métier pures

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


Cas d’Usage

Cette approche est idéale pour :

  • ✅ APIs REST externes (désérialisation JSON)
  • ✅ Sessions Redis avec métadonnées dynamiques
  • ✅ Cache avec données arbitraires
  • ✅ Configuration utilisateur personnalisable
  • ✅ Webhooks/intégrations externes avec structure variable

Prochaines étapes

Maintenant que vous maîtrisez les DTOs et Jackson, explorez comment créer des ports et adapters concrets.

Last updated on