# Polymorphic DTO using Java Record with Jackson

With the introduction of `Java Record` class, it becomes the perfect candidate to use as a `Data Transfer Object (DTO)`. I'm not going to explain more, you can find tons of articles out there with its benefit and code samples.

However, what is missing out there is a code sample of how to use it with a Polymorphic DTO use case where we know that `Java Record` classes cannot be extended but can implement interfaces.

I'm going to show you how we can achieve this together with `Jackson`

# Scenario

Imagine we have an API endpoint that accepts `HomeAddressDto` and `OfficeAddressDto` as the `@RequestBody`, which would look like the following:

```java
public record HomeAddressDto(String street, String postalCode, String unit) {}

public record OfficeAddressDto(String building, String street, String postalCode, String unit) {}

@RestController
@RequestMapping("/addresses")
public class AddressController {

    @PostMapping("/home")
    public HomeAddressDto create(@RequestBody HomeAddressDto homeAddressDto) {
        return homeAddressDto;
    }

    @PostMapping("/office")
    public OfficeAddressDto create(@RequestBody OfficeAddressDto officeAddressDto) {
        return officeAddressDto;
    }
}
```

Now, there is nothing wrong with this, and it works, but it can be pretty ugly having to create different endpoints to cater to each different (but of the same type) DTO. It can get messy real fast.

Let's see if we can improve this.

# Solution

## Step 1: Common Interface

Since we know that both types are address, we can create an empty interface to house both classes.

> Remember, `Java Record` cannot be extended

```java
public interface Address {}
```

Then we update both classes to implement `Address` interface

```java
public record HomeAddressDto(String street, String postalCode, String unit) implements Address {}

public record OfficeAddressDto(String building, String street, String postalCode, String unit) implements Address {}
```

This way, it allows us to specify `Address` as the `@RequestBody`

## Step 2: Common Endpoint

Given that we have `Address` interface, we probably can work on having a single endpoint to cater to both types of address classes.

```java
@PostMapping
public Address create(@RequestBody Address address) {
    return address;
}
```

Let's try to hit that new endpoint

```bash
curl -X POST -H "Content-Type: application/json" -d "@address.json" localhost:8080/addresses | jq '.'
```

*   Perform a curl POST request to localhost:8080/addresses
    
*   Using `address.json` content
    
*   Format output (pretty print) using `jq`
    

The content of `address.json` is

```json
{
    "street": "st",
    "postalCode": "pc",
    "unit": "u"
}
```

And this is what we will get back

```json
{
  "timestamp": "2022-12-20T14:44:10.836+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "trace": "org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]\r\n\tat", // omitted
  "message": "Type definition error: [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]",
  "path": "/addresses"
}
```

So what happens? Because the `@RequestBody` is using an interface, `Jackson` is not able to construct the class, hence the error message "abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information"

Is it possible to overcome this? Fortunately, yes, and it is commonly used in `Java Classes` with inheritance. But in this case, we want to do it in `Java Record`

## Step 3: Jackson Polymorphism Support

`Jackson` has support for polymorphic classes using `@JsonTypeInfo` and `@JsonSubTypes`, but almost all samples (online) are used based on `Class` instead of `Record`.

### Deduction Based Polymorphism

This is the first approach that I am going to show

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
    @Type(HomeAddressDto.class),
    @Type(OfficeAddressDto.class)
})
public interface Address {}
```

*   Using `Deduction` based approach (available since 2.12)
    
*   Declare the various subclasses
    

If your subclasses have distinct fields, this approach is the easiest, as Jackson will assign to the correct class based on the field.

In this case, the difference between `HomeAddressDto` and `OfficeAddressDto` is that `OfficeAddressDto` has an additional field - `building`. If we fire the same API request, this is what we will get back

```json
{
  "timestamp": "2022-12-20T14:58:25.515+00:00",
  "status": 400,
  "error": "Bad Request",
  "trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Could not resolve subtype of [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]: Cannot deduce unique subtype of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (2 candidates match); nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]: Cannot deduce unique subtype of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (2 candidates match)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream)", // omitted
  "message": "JSON parse error: Could not resolve subtype of [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]: Cannot deduce unique subtype of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (2 candidates match); nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address]: Cannot deduce unique subtype of `com.bwgjoseph.springbootpolymorphicjavarecordwithjackson.deduction.Address` (2 candidates match)\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 59]",
  "path": "/addresses"
}
```

The error message, "2 candidates match" tells us that it is not able to automatically deduce based on the given field and thus, it throws the error. If we update the `json` to include `building` field

```json
{
	"building": "building",
    "street": "st",
    "postalCode": "pc",
    "unit": "u"
}
```

And run the same request again. This time, it will be successful and returns the following response.

```json
{
  "building": "building",
  "street": "st",
  "postalCode": "pc",
  "unit": "u"
}
```

It works, but as I have mentioned earlier, this is great if you have distinct fields to allow `Jackson` to make the correct deduction. Although, it is also possible to tell `Jackson` which is the default class to deduce into

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = HomeAddressDto.class)
@JsonSubTypes({
    @Type(HomeAddressDto.class),
    @Type(OfficeAddressDto.class)
})
public interface Address {}
```

*   Notice the `defaultImpl = HomeAddressDto.class`, this tells `Jackson` to fall back to this class if you can't figure out which one to match
    

If it works for your use case, then great, otherwise, there is also another "strategy" you can use that is `Name` based approach

### Name Based Polymorphism

This approach relies on a separate field to tell `Jackson` how to map to the correct subclass.

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
    @Type(value = HomeAddressDto.class, name = "home"),
    @Type(value = OfficeAddressDto.class, name = "office")
})
public interface Address {}
```

*   We now use `NAME` instead of `DEDUCTION` in `@JsonTypeInfo`
    
*   We now specify `name` within the `JsonSubTypes`, giving its unique identifier
    

With that, we have to hint `Jackson` by annotating `@JsonTypeName` on the individual class

```java
@JsonTypeName("home")
public record HomeAddressDto(String street, String postalCode, String unit) implements Address {}

@JsonTypeName("office")
public record OfficeAddressDto(String building, String street, String postalCode, String unit) implements Address {}
```

> `@JsonTypeName` has to match the name defined in `@JsonSubTypes`

Next, update the `address.json` to include `@type` field

```json
{
    "street": "st",
    "postalCode": "pc",
    "unit": "u",
	"@type": "home"
}
```

*   Added `@type` field where the value should match the value that is defined in `@JsonSubTypes`
    

And if we were to run the request via curl again. This would be the response.

```json
{
  "@type": "home",
  "street": "st",
  "postalCode": "pc",
  "unit": "u"
}
```

### Enum Based Polymorphism

This last approach is pretty much the same as the previous method, only that we are going to use `enum` instead of a `string` value.

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @Type(value = HomeAddressDto.class, name = "home"),
    @Type(value = OfficeAddressDto.class, name = "office")
})
public interface Address {}

public record HomeAddressDto(String street, String postalCode, String unit) implements Address {}

public record OfficeAddressDto(String building, String street, String postalCode, String unit) implements Address {}
```

*   Notice that we added `property`
    
*   It no longer requires adding `@JsonTypeName` on the respective `Dto`
    

We need to create a new `enum` class

```java
public enum Type {
    HOME,
    OFFICE
}
```

And if we were to run the request via curl again. This would be the response.

```json
{
  "type": "home",
  "street": "st",
  "postalCode": "pc",
  "unit": "u"
}
```

# Conclusion

We have seen how to use Polymorphic DTO using Java Record combined with Jackson to make our endpoint much cleaner and easier to manage.

I am not aware of any downside to this approach yet, or if it does not work for the more complicated use cases. If you know any, drop a comment below!

# Source Code

As usual, the full source code is available on [GitHub](https://github.com/bwgjoseph/tutorials/tree/main/spring-boot-polymorphic-java-record-with-jackson)
