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 -parameter
argument, so the argument names are preserved in the compiled code.
Luckily the -parameter
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:
...
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 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.