Search

Dark theme | Light theme

August 16, 2018

Micronaut Mastery: Decode JSON Using Custom Constructor Without Jackson Annotations

Micronaut uses Jackson to encode objects to JSON and decode JSON to objects. Micronaut adds a Jackson ObjectMapper bean to the application context with all configuration to work properly. Jackson can by default populate an object with values from JSON as the class has a no argument constructor and the properties can be accessed. But if our class doesn't have a no argument constructor we need to use the @JsonCreator and @JsonProperty annotations to help Jackson. We can use these annotation on the constructor with arguments that is used to create an object.

But we can even make it work without the extra annotations, so our classes are easier to read and better reusable. We need to add the Jackson ParameterNamesModule as module to the ObjectMapper instance in our application. And we need to compile our sources with the -parameters argument, so the argument names are preserved in the compiled code. Luckily the -parameters option is already added to our Gradle build when we create a Micronaut application. All we have to do is to add the ParameterNamesModule in our application.

We need to add a dependency on com.fasterxml.jackson.module:jackson-module-parameter-names to our compile class path. Micronaut will automatically find the ParameterNamesModule and add it to the ObjectMapper using the findAndRegisterModules method.

So we change our build.gradle file first:

// File: build.gradle
...
dependencies {
    ...
    compile "com.fasterxml.jackson.module:jackson-module-parameter-names:2.9.0"
    ...
}
...

In our application we have the following class that has an argument constructor to create a immutable object:

package mrhaki;

import java.util.Objects;

public class Language {
    private final String name;

    private final String platform;

    public Language(final String name, final String platform) {
        this.name = name;
        this.platform = platform;
    }

    public String getName() {
        return name;
    }

    public String getPlatform() {
        return platform;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        final Language language = (Language) o;
        return Objects.equals(name, language.name) &&
                Objects.equals(platform, language.platform);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, platform);
    }
}

We write the following sample controller to use the Language class as a return type (we wrap it in a Mono object so the method is reactive):

package mrhaki;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import reactor.core.publisher.Mono;

@Controller("/languages")
public class LanguagesController {

    @Get("/groovy")
    public Mono<Language> getGroovy() {
        return Mono.just(new Language("Groovy", "JVM"));
    }

}

And in our test we use HttpClient to invoke the controller method. The exchange method will trigger a decode of the JSON output of the controller to a Language object:

package mrhaki

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class LanguagesControllerSpec extends Specification {

    @AutoCleanup
    @Shared
    private static EmbeddedServer server = ApplicationContext.run(EmbeddedServer)

    @AutoCleanup
    @Shared
    private static HttpClient client = server.applicationContext.createBean(HttpClient, server.URL)

    void '/languages/groovy should find language Groovy'() {
        given:
        final request = HttpRequest.GET('/languages/groovy')

        when:
        final response = client.toBlocking().exchange(request, Language)

        then:
        response.status() == HttpStatus.OK

        and:
        response.body() == new Language('Groovy', 'JVM')
    }


}

When we wouldn't have the dependency on jackson-module-parameter-names and not use the -parameter compiler option we get the following error message:

[nioEventLoopGroup-1-6] WARN  i.n.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.micronaut.http.codec.CodecException: Error decoding JSON stream for type [class mrhaki.Language]: Cannot construct instance of `hello.conf.Language` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"name":"Groovy","platform":"JVM"}"; line: 1, column: 2]

But with the dependency and -parameter compiler option we have a valid test without any errors. Jackson knows how to use the argument constructor to create a new Language object.

Written with Micronaut 1.0.0.M4.