BluePrint
BluePrint

Reputation: 2134

Java ModelMapper: How do I map a nested record to another nested record with a property being an ENUM

I have two different records that I am trying to map using ModelMapper.

Origin record:

package com.myapp.transport.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record MetricsDto(
    @JsonProperty("category_id") Long categoryId,
    @JsonProperty("source") String source,
    @JsonProperty("data") MetricsDataDto data
) {
public record MetricsDataDto(
        @JsonProperty("c_user_defined_parameters") Double cUserDefinedParameters,
        @JsonProperty("c_system_defined_parameters") Double cSystemDefinedParameters,
        @JsonProperty("t_user_applied_line_cost") Double tUserAppliedLineCost,
        @JsonProperty("t_user_identified_cost") Double tUserIdentifiedCost,
        @JsonProperty("t_user_cost") Double tUserCost
    ) {}
}

Destination record:

package com.myapp.domain.model;

public record Metrics(
    Long categoryId,
    Source source,
    MetricsData data
) {
    public record MetricsData(
        Double cUserDefinedParameters,
        Double cSystemDefinedParameters,
        Double tUserAppliedLineCost,
        Double tUserIdentifiedCost,
        Double tUserCost
    ) {}
}

Note that the data objects in both records are much larger, about 50 items. Therefore I prefer using records instead of classes to avoid having 50 different getters and setters.

Also, as you can see, the destination record has the source property as an enum Source instead of as a string:

package com.myapp.domain.util;

public enum Source {
    EXTERNAL() {
        @Override
        public String toString() {
            return "external";
        }
    },
    INTERNAL() {
        @Override
        public String toString() {
            return "internal";
        }
    },
    COMBINED() {
        @Override
        public String toString() {
            return "combined";
        }
    };

    public static Source fromString(String source) throws IllegalArgumentException {
        return switch (source) {
            case "external" -> EXTERNAL;
            case "internal" -> INTERNAL;
            case "combined" -> COMBINED;
            default -> throw new IllegalArgumentException("Unknown source: " + source);
        };
    }
}

I am trying to use ModelMapper to map from the origin record to the destination record:

package com.myapp.transport.controller;

import com.myapp.domain.model.Metrics;
import com.myapp.domain.util.Source;
import com.myapp.transport.dto.MetricsDto;
import org.modelmapper.TypeMap;
import org.modelmapper.record.RecordModule;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(Endpoints.METRICS)
public class MetricsController {

    private final ModelMapper modelMapper = new ModelMapper().registerModule(new RecordModule());

    @PostMapping(value = Endpoints.BULK_ADD, consumes = Constants.APPLICATION_JSON)
    public ResponseEntity<Object> bulkAdd(@RequestBody List<MetricsDto> metricsDtos) {
        List<Metrics> metrics = metricsDtos
            .stream()
            .map(metricsDto -> modelMapper.map(metricsDto, Metrics.class))
            .toList();

        // ... other code that will use the metrics object
    }
}

However this does not work:

 Failed to instantiate instance of destination com.myapp.domain.model.Metrics. Ensure that com.myapp.domain.model.Metrics has a non-private no-argument constructor.

I assume that there is a problem with

Can someone please help me check what I'm doing wrong.

Upvotes: 0

Views: 80

Answers (1)

Ale Zalazar
Ale Zalazar

Reputation: 1980

ModelMapper would not support records by default (or classes with no-args constructor undefined) although in this case you can create a provider for the modelMapper configuration to define how a destination instance should be instantiated. Here's a minimal example based on the ModelMapper Provider docs (https://modelmapper.org/user-manual/providers/):

        ModelMapper modelMapper = new ModelMapper();
        // Custom provider for Person record
        modelMapper.getConfiguration().setProvider(context -> {
            Map<String, Object> source = (Map<String, Object>) context.getSource();
            return new Person((String) source.get("name"), (Integer) source.get("age"));
        });

        Map<String, Object> map = Map.of("name", "Alice", "age", 25);
        Person person = modelMapper.map(map, Person.class);
        System.out.println(person); // Output should be: Person[name=Alice, age=25] 

Upvotes: 1

Related Questions