Deserializing Polymorphic Lists with Kotlin, Jackson, and Spring Boot
Tags: kotlin, typescript, serialization
Published: 2023-11-25 06:52:16

---
title: Deserializing Polymorphic Lists with Kotlin, Jackson, and Spring Boot
published: true
date: 2023-11-25 05:05:20 UTC
tags: kotlin, typescript, serialization
canonical_url: https://macarthur.me/posts/handling-polymorphic-lists
---

One of my favorite features my of TypeScript is _discriminated unions,_ in which a common literal type is used to determine the narrowest possible type of an object. Think of two types of messages – `Email` and `SMS`, with types defined as such:

```
interface Email {
	kind: 'email'; // <- literal type
	subject: string;
	from: string;
	to: string;
	body: string;
}

interface SMS {
	kind: 'sms'; // <- literal type
	sender: string;
	recipient: string;
	message: string;
}

// A union type, discriminated by "kind":
type Message = Email | SMS;

// TypeScript error:
// Property 'content' is missing in type 
// '{ kind: "sms"; sender: string; recipient: string; }' 
// but required in type 'SMS'.
const myMessage: Message = {
	kind: 'sms',
	sender: '+11234567890',
	recipient: '+10987654321',
};

```

Since `myMessage` has a literal of `kind: 'sms'`, TypeScript knows it's an `SMS`, prompting it for the missing `content` property. No further configuration necessary.

## Submitting a Polymorphic List to a REST Endpoint

This is a useful feature for a number of cases. One of them is submitting a list of data composed of multiple types (a _polymorphic_ list) to a REST endpoint. Here's an example with Fastify, using the same `Message` type defined above.

![example fastify route showing typescript error](https://cms.macarthur.me/content/images/2023/11/image-13.png)

As you can see, when we attempt to pull `content` off of what TypeScript knows is an `Email`, it gets mad. For good reason!

![typescript error message](https://cms.macarthur.me/content/images/2023/11/image-14.png)

Thanks to that discriminated union, it _knows_ that if it's an "email" it can't have a `content` property, preventing us from writing bugs we'd otherwise only find at runtime.

### Validating the Payload at Runtime

As a secondary benefit, because we're using Fastify, it's easy to validate that incoming request too, albeit much more verbosely than simply defining some types. Here's how that'd look:

```
const emailSchema = {
	type: "object",
	required: ["kind", "subject", "from", "to", "body"],
	properties: {
		kind: { type: "string", enum: ["email"] },
		subject: { type: "string" },
		from: { type: "string" },
		to: { type: "string" },
		body: { type: "string" },
	},
};

const smsSchema = {
	type: "object",
	required: ["kind", "sender", "recipient", "content"],
	properties: {
		kind: { type: "string", enum: ["sms"] },
		sender: { type: "string" },
		recipient: { type: "string" },
		content: { type: "string" },
 	},
};

server.addSchema({
	$id: "message",
	oneOf: [emailSchema, smsSchema],
});
```

We can then tell Fastify to require each item in the provided array to be a "message."

```
server.post<{
  Body: Array<Message>;
}>(
	"/submit",
	{
		schema: {
			body: {
				type: "array",
				items: { $ref: "message#" },
			},
		},
	},
 	(request, reply) => {
		// handle it
	}
);
```

And we'd get a clear error message if we left any required property off:

![fastify validation error message](https://cms.macarthur.me/content/images/2023/11/image-15.png)

In all, we've got some really nice guardrails to safely reason about everything once we've made it past the request boundary.

## Things Look Different in Spring Boot, Jackson, and Kotlin

I'm not that well-versed in the JVM world. I started working in it only a couple of years ago, and it hasn't even been consistent. Still, I was hopeful this sort of convenience would exist when I wanted to submit a similar sort of list in a Spring Boot application, built with Kotlin and using the [Jackson serialization library](https://github.com/FasterXML/jackson?ref=alex-macarthur). Think of a simple endpoint accepting a list of `Message` objects:

```
@PostMapping("/submit")
fun submitMessages(
	@RequestBody
	messages: List<Message>,
): String {
	// do stuff
}
```

As it turns out, it doesn't have the same ergonomics as TypeScript's discriminated unions, but it's still possible to get it done. To pull it off, we're gonna use a sealed class, a neat Kotlin feature useful for creating a restricted class hierarchy. In an ideal world, this is how that class would look:

```
sealed class Message {
	data class Email(
		val subject: String,
		val from: String,
		val to: String,
		val body: String,
	) : Message()

	data class SMS(
		val sender: String,
		val recipient: String,
		val content: String,
	) : Message()
}
```

It's pretty tidy, and after deserialization, it'd let us use Kotlin's `when` operator to handle each subtype as needed, despite both of them being a `Message`:

```
messages.map {
	when (it) {
		is Message.Email -> print("email!")
		is Message.SMS -> print("sms!")
	}
}
```

Unfortunately, in order for Jackson to deserialize the request payload correctly, that sealed class will need to get a little more complex.

### Annotating the Model

First up, we'll add two annotations to our `Message` class that Jackson will use to determine how to handle the types within our submitted list.

```
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "kind",
)
@JsonSubTypes(
	JsonSubTypes.Type(value = Message.Email::class, name = "email"),
	JsonSubTypes.Type(value = Message.SMS::class, name = "sms"),
)
```

A brief breakdown:

- The `@JsonTypeInfo` annotation tells Jackson that a "kind" property will exist on each item in the payload. 
- The `@JsonSubTypes` annotation tells Jackson which object within our sealed class to instantiate, based on whatever was passed as the value of "kind." 

To put it another way: when an item has a "kind" of "email," Jackson will instantiate a new `Message.Email`. When it's "sms," it'll create a `Message.SMS`.

### Modifying Our Sealed Class Subclasses

Since we're now relying on the "kind" property to tell Jackson what to instantiate, we need to refactor the members our sealed class a bit too. Here's how it'll look:

```
data class EmailData(
	val subject: String,
	val from: String,
	val to: String,
	val body: String,
)

data class SMSData(
	val sender: String,
	val recipient: String,
	val content: String,
)

// Aformentioned annotations go here.
sealed class Message {
    data class Email(
        @JsonProperty("message")
        val message: EmailData,
    ) : Message()

    data class SMS(
        @JsonProperty("message")
        val message: SMSData,
    ) : Message()
}
```

As you can see, neither shape has its information declared as top-level properties anymore. Since we needed to make room for a `kind` of message, it's been relegated to a `message` property, with the details being extracted to `EmailData` and `SMSData` classes.

Not as clean as I'd like it to be, but it'll do the job.

### Validating Everything

We can test this out by modifying our endpoint to spit back the messages we provide. Obviously, in a production application, you'd be doing something more interesting.

```
@PostMapping("/submit")
fun submitMessages(
	@RequestBody
	messages: List<Message>,
): String = messages.map {
	when (it) {
		is Message.Email -> "email"
		is Message.SMS -> "sms"
	}
}.joinToString { it }
```

We'll hit it with the following payload:

```
[
    {
		"kind": "sms",
		"message": {
			"sender": "+11234567890", 
			"recipient": "+10987654321",
			"content": "hey"
		}
	},
	{
		"kind": "email",
		"message": {
			"subject": "a subject", 
			"from": "me@example.com", 
			"to": "you@example.com", 
			"body": "hello"
		}
	}
]
```

Spring is able to deserialize the payload into the objects we expect, and we get back what we intended:

![the expected response from a POST request](https://cms.macarthur.me/content/images/2023/11/image-11.png)

And just by relying on plain, ol' Kotlin types, we get some useful feedback when submitting invalid data too. Here's what we get just by leaving off `"content": "hey"` in the above payload:

![a 400 response caused by a missing property](https://cms.macarthur.me/content/images/2023/11/image-12.png)

Based on what we set as the message "kind," Spring knows exactly what to enforce on the incoming request, even without using a more mature validation library like Javax or Jakarta (which are recommended in more fleshed out applications, by the way).

So, any client will get the feedback they need when attempting to submit data, and our application will have full type knowledge when handling that request to completion.

## Shout-Out to a Potentially Better Design

I've heard feedback that designing a request payload like this might be more of an interesting, "smelly" design than a _good_ one. A better approach might be to accept a `MessagePayload` model, which would hold both `SMS` and `Email` messages. Something like this (beware... untested code):

```
sealed class Message {
    data class Email(
		val subject: String,
		val from: String,
		val to: String,
		val body: String,
    ) : Message()

    data class SMS(
		val sender: String,
		val recipient: String,
		val content: String
    ) : Message()
}

data class MessagePayload(
    val smsMessages: List<Message.SMS>,
    val emailMessages: List<Message.Email>
)
```

Admittedly, it does read easier, and would require less dependence on Jackson's annotations to inform Kotlin how to build out the items within the payload. I think there's a lot of value in that. In general, incline yourself toward simpler, more readable code.

But... there also doesn't seem to be a demonstrable benefit to this over a polymorphic approach. In the end, the way we're handling a polymorphic list gives us full type safety + request validation, and it might also expose a more useful interface to whatever clients will be using the endpoint.

Like any other engineering problem, it can probably be reduced to "it depends." Even so, I'm curious to hear your thoughts on how to simplify this, and whether there are legitimate reasons to avoid such a design in favor of another.