Logo

dev-resources.site

for different kinds of informations.

Painless JSON with Kotlin and jackson

Published at
4/10/2020
Categories
kotlin
json
jackson
rest
Author
sirech
Categories
4 categories in total
kotlin
open
json
open
jackson
open
rest
open
Author
6 person written this
sirech
open
Painless JSON with Kotlin and jackson

It seems that many backends that provide a REST API end up being glorified proxies that move JSON from one place to another. It is especially true if you are trying to keep those backends as simple as possible (Microservices anyone?). Having the right tools to parse and produce JSON can thus make a big impact in keeping the code tidy and compact. I want to talk about my experience using Kotlin and Jackson for this.

I remember that dealing with JSON in Java used to be pretty painful back in the day, as you had to write a ton of code to map objects. That is what initially led me to use Ruby. Things have changed a lot (for the better!) since then. Nowadays, using Kotlin and Jackson you can deal with JSON with minimal effort. Jackson is a mighty library, but you can get lost easily. I have a bunch of examples showing how to parse different classes, plus some code to integrate it into your workflow. In this case, I will be using SpringBoot.

Serialize/Deserialize

We will be using data classes to represent the entities that will get converted to a from JSON. They are the equivalent of using the @Value annotation in Lombok, with first-class support from the language. They are immutable (yay!) and have convenience methods like equals and toString out of the box.

You can use an ObjectMapper to do the parsing, although you can configure SpringBoot to do it mostly automatically, which I will show later. I have a User entity with two fields that I want to convert to JSON and back.

data class User(val id: String, val age: Int)

fun User.toJson(): String = ObjectMapper().writeValueAsString(this)
fun String.toUser(): User = ObjectMapper().readValue(this)
Enter fullscreen mode Exit fullscreen mode

For simple cases, just defining the data class is enough, as long as you have the right module. There are a bunch of extra configurations that you can do on top of it, though. Many of them can be controlled with annotations, which make the code a lot more compact. Abusing them will turn your code into an unmaintainable mess, though.

Nullability

If some of the fields are optional, you provide default values.

data class User(
  val id: String = ""
)
Enter fullscreen mode Exit fullscreen mode

You can also allow them to be null.

data class User(
  val id: String?
)
Enter fullscreen mode Exit fullscreen mode

not doing anything will make the parsing fail with an exception, which I find a good thing.

Aliasing

If you are parsing your object from a different source that uses different attribute names, but still want to keep a canonical representation, @JsonAlias is your friend.

data class User(
  @JsonAlias("userId")
  val id: String
)
Enter fullscreen mode Exit fullscreen mode

this will correctly parse something like

{
  "userId": "123"
}
Enter fullscreen mode Exit fullscreen mode

Ignore properties

Maybe you are parsing an object with a ton of fields that you don’t need. If you are not going to use it in your code, you really should avoid adding them, as that makes it harder to understand what is needed and what is not. @JsonIgnoreProperties can be used for this.

@JsonIgnoreProperties(ignoreUnknown = true)
data class User(val id: String)
Enter fullscreen mode Exit fullscreen mode

Different representations

If your backend is acting as a proxy, you will be reading your data from somewhere and passing it to your client. In this case, you might want to skip some fields in the serialization to give your client precisely the fields it needs. You can accomplish this by customizing the access property.

data class User(
  val id: String = "",
  @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
  val age: Int
)
Enter fullscreen mode Exit fullscreen mode

The serialization of this object won’t contain the age, but it is available in our code. This approach does not scale that well, however. If you find that you have two different representations of the same entity and are adding a ton of annotations to use one class, it’s probably better to split it into two distinct classes and provide a method one into the other.

This underscores an important point. You don’t need to use annotations and implicit conversions for everything. In some places having dedicated converters is just more readable, more so if you want to attach some logic to that process.

And if you want more…

This article shows just a small part of what is possible to do. You can control every aspect of the serialization/deserialization process. Have a look at this post if you want to know about other options.


Getting away from untyped strings

In JSON you tend to use strings to represent many entities. Any id type like a user id, or something like a language code, for example. I prefer mapping them to dedicated classes in my code. I’ve seen many bugs where the wrong entity is used when that could be prevented directly by the compiler. Taking a UserId as an example, I like to model it as follows:

  • It should be an immutable data class
  • It should not force a change in the structure of the JSON (i.e., no nesting)
  • Serialize/Deserialize should work out of the box
data class UserId(private val value: String) {
    companion object {
        @JvmStatic
        @JsonCreator
        fun create(value: String) = UserId(value.toLowerCase())
    }

    @JsonValue
    override fun toString() = value
}
Enter fullscreen mode Exit fullscreen mode

By using a data class, we get an immutable object that represents this entity. We can do relatively little with it. In fact, we don’t even want access to the internal fields. We are going to compare instances directly, and if we need to get a string representation, we’ll do that through the toString method.

The serialization happens through the @JsonValue annotation, where we use the value directly. If we modify our User class that we have been using before, it will look like this.

data class User(val id: UserId, val age: Int)
Enter fullscreen mode Exit fullscreen mode

That class serializes to this JSON

{
  "id": "123",
  "age": 20
}
Enter fullscreen mode Exit fullscreen mode

That representation matches how most clients (especially a frontend) would expect this structure to look like, without sacrificing any safety in the backend.

The deserialization happens automatically. However, I like to define a static constructor (using the @JvmStatic and @JsonCreator annotations) so that I can do things like sanitizing the input before generating my instance. This helps to make sure our models are in a consistent state.

Since Kotlin 1.3, a new concept called inline classes has been introduced, which might match better with this use case. Jackson has some trouble deserializing it properly in nested objects as of 16/06/19, so I could not replace my data classes with it so far. There is an open issue in Github to follow.


SpringBoot integration

Here we get the last piece of the puzzle. We can manually use an ObjectMapper and convert things explicitly. It is much easier if that happens on its own. The good news is that there is not much to do here other than adding the jackson-module-kotlin as a dependency:

implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}")
Enter fullscreen mode Exit fullscreen mode

If you are using the latest versions of everything, that will be enough. If the spring magic does not happen on its own (spring does a lot of magic that I don’t quite understand), you can do it manually. You can use a @Configuration so that your controllers can map to and from JSON automatically.

@Configuration
class JacksonConfiguration {
    @Bean
    fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
        return MappingJackson2HttpMessageConverter().apply {
            this.objectMapper = ObjectMapper().apply {
                registerModule(KotlinModule())
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

if you are making REST request to another service, you can build a custom RestTemplate doing the same:

open class DefaultRestTemplate(baseUrl: String) :
        RestTemplate(HttpComponentsClientHttpRequestFactory(
                HttpClientBuilder.create().useSystemProperties().build())) {
    init {
        uriTemplateHandler = DefaultUriBuilderFactory(baseUrl)
        messageConverters = jacksonConverter(messageConverters)
    }

    private fun jacksonConverter(converters: MutableList<HttpMessageConverter<*>>): List<HttpMessageConverter<*>> {
        val jsonConverter = MappingJackson2HttpMessageConverter()
        jsonConverter.objectMapper = jacksonObjectMapper().apply {
            registerModule(KotlinModule())
        }
        converters.add(jsonConverter)
        return converters
    }
}
Enter fullscreen mode Exit fullscreen mode

Again, all this should happen by just adding the library to the classpath. Use this as a fallback in case that does not work for some reason. Also, this template can be extended to use a base URL, receive environment variables (to include the keystore for instance), or automatically add certain headers to your requests.

PathVariables are not JSON

Now that we are waist-deep in automated JSON mapping, I’m getting ambitious. As mentioned above, we are no longer using plain strings but proper domain classes. Let’s say you have a route like GET /users/:userId. The controller would look like this:

@RestController
@RequestMapping("/users", produces = [MediaType.APPLICATION_JSON_VALUE])
class HelloController {
    @GetMapping("{userId}")
    fun user(@PathVariable("userId") userId: UserId): ResponseEntity<User>
}
Enter fullscreen mode Exit fullscreen mode

If you send a request to this route, the userId will get parsed automatically, but our custom create method won’t get called, because this is a URL, not JSON. We didn’t come this far to start parsing strings manually again. Let’s fix this by using a custom converter.

@Configuration
class ConverterConfiguration : WebMvcConfigurer {
    override fun addFormatters(registry: FormatterRegistry) {
        registry.addConverter(userId())
    }

    private fun userId(): Converter<String, UserId> {
        return Converter<String, UserId> { source -> UserId.create(source) }
    }
}
Enter fullscreen mode Exit fullscreen mode

That’s it. Now we can be sure that those pesky strings are not floating through our app at any point in the flow of a request.

Summary

Jackson is a very powerful library, and to be honest, you can overdo it with all the annotations. If you use them judiciously, though, working with JSON becomes very easy while keeping a good amount of type safety in the process. For testing, this goes well with recorded APIs using WireMock.

EDIT 25/12/2019: Grammar review

jackson Article's
30 articles in total
Favicon
Why Do We Still Need Jackson or Gson in Java?
Favicon
A simple GeoJSON serializer for Jackson
Favicon
[Java Spring Boot] Como Criar Serializador Personalizado para seus Responses ou Json de saída
Favicon
[Java Spring Boot] How to implement a Custom Serializer for your Responses or Json
Favicon
[Java SpringBoot] Como Criar Deserializador Personalizado para seus Requests
Favicon
[Java SpringBoot] How to implement a Custom Deserializer for your Requests
Favicon
Java Jackson JSON: How to Handle Custom Keys?
Favicon
Create a custom Jackson JsonSerializer und JsonDeserializer for mapping values
Favicon
Anotación @JsonUnwrapped
Favicon
A tale of fixing a tiny OpenAPI bug
Favicon
Kotlin Springboot -- Part 21 任意の key value の json を POST する API E2E を書く
Favicon
Formatting json Date/LocalDateTime/LocalDate in Spring Boot
Favicon
Jackson's @JsonView with SpringBoot Tutorial
Favicon
Jackson JSON parsing top-level map into records
Favicon
Using Jackson Subtypes to Write Better Code
Favicon
Java – Convert Excel File to/from JSON (String/File) – using Apache Poi + Jackson
Favicon
How to resolve Json Infinite Recursion problem when working with Jackson
Favicon
Java – Convert Excel File to/from JSON (String/File) – using Apache Poi + Jackson
Favicon
Practical Java 16 - Using Jackson to serialize Records
Favicon
Kotlin – Convert Object to/from JSON with Jackson 2.x
Favicon
💾 Java Records 💿 with Jackson 2.12
Favicon
Jackson, JSON and the Proper Handling of Unknown Fields in APIs
Favicon
Polymorphic deserialization with Jackson and no annotations
Favicon
Playing around with Kotlin Sealed Classes
Favicon
Moonwlker: JSON without annotation
Favicon
Jackson Readonly properties and swagger UI
Favicon
Registering Jackson sub-types at runtime in Kotlin
Favicon
Parsing JSON in Spring Boot, part 1
Favicon
Customize how Jackson does LocalDate Parsing
Favicon
Painless JSON with Kotlin and jackson

Featured ones: