I'm developing simple bot for my telegram channel and I'm using Java 16 features, like record classes. The issue is that I can't deserialize incoming requests into record classes. I'm using Jackson with Micronaut to setup a client. Here are my classes:
User record
public record User(
long id,
boolean isBot,
String firstName,
String userName,
boolean canJoinGroups,
boolean canReadAllGroupMessages,
boolean supportsInlineQueries) {
}
TelegramResponse record
public record TelegramResponse<T>(T result, boolean ok) {
}
TelegramApiClient class
@Client("https://api.telegram.org/bot${bot.id}")
public interface TelegramApiClient {
@Get("/getMe")
TelegramResponse<User> getSelf();
}
When I call this method, I'm getting this error:
16:22:52.916 [default-nioEventLoopGroup-1-2] ERROR i.m.h.s.netty.RoutingInBoundHandler - Unexpected error occurred: Error decoding HTTP response body: Error decoding stream for type [class com.praytic.TelegramResponse]: Can not set final java.lang.Boolean field com.praytic.User.supportsInlineQueries to java.lang.Boolean (through reference chain: com.praytic.TelegramResponse["result"])
io.micronaut.http.client.exceptions.HttpClientResponseException: Error decoding HTTP response body: Error decoding stream for type [class com.praytic.TelegramResponse]: Can not set final java.lang.Boolean field com.praytic.User.supportsInlineQueries to java.lang.Boolean (through reference chain: com.praytic.TelegramResponse["result"])
at io.micronaut.http.client.netty.DefaultHttpClient$11.channelReadInstrumented(DefaultHttpClient.java:2191)
at io.micronaut.http.client.netty.DefaultHttpClient$11.channelReadInstrumented(DefaultHttpClient.java:2061)
at io.micronaut.http.client.netty.DefaultHttpClient$SimpleChannelInboundHandlerInstrumented.channelRead0(DefaultHttpClient.java:2765)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.micronaut.http.netty.stream.HttpStreamsHandler.channelRead(HttpStreamsHandler.java:194)
at io.micronaut.http.netty.stream.HttpStreamsClientHandler.channelRead(HttpStreamsClientHandler.java:183)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1368)
at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1234)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1280)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:507)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:446)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: io.micronaut.http.codec.CodecException: Error decoding stream for type [class com.praytic.TelegramResponse]: Can not set final java.lang.Boolean field com.praytic.User.supportsInlineQueries to java.lang.Boolean (through reference chain: com.praytic.TelegramResponse["result"])
at io.micronaut.jackson.codec.JacksonMediaTypeCodec.decode(JacksonMediaTypeCodec.java:209)
at io.micronaut.http.client.netty.FullNettyClientHttpResponse.convertByteBuf(FullNettyClientHttpResponse.java:280)
at io.micronaut.http.client.netty.FullNettyClientHttpResponse.lambda$getBody$1(FullNettyClientHttpResponse.java:218)
at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1224)
at io.micronaut.http.client.netty.FullNettyClientHttpResponse.getBody(FullNettyClientHttpResponse.java:192)
at io.micronaut.http.client.netty.FullNettyClientHttpResponse.<init>(FullNettyClientHttpResponse.java:111)
at io.micronaut.http.client.netty.DefaultHttpClient$11.channelReadInstrumented(DefaultHttpClient.java:2121)
... 52 common frames omitted
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not set final java.lang.Boolean field com.praytic.User.supportsInlineQueries to java.lang.Boolean (through reference chain: com.praytic.TelegramResponse["result"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:274)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:623)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:611)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:634)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.set(FieldProperty.java:193)
at com.fasterxml.jackson.databind.deser.impl.PropertyValue$Regular.assign(PropertyValue.java:62)
at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:211)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:520)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1405)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:565)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:449)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1405)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3643)
at io.micronaut.jackson.codec.JacksonMediaTypeCodec.decode(JacksonMediaTypeCodec.java:204)
... 58 common frames omitted
Caused by: java.lang.IllegalAccessException: Can not set final java.lang.Boolean field com.praytic.User.supportsInlineQueries to java.lang.Boolean
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79)
at java.base/java.lang.reflect.Field.set(Field.java:793)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.set(FieldProperty.java:190)
... 74 common frames omitted
I have read from this article that Jackson 2.12 already supports java.lang.Record type. My Jackson configuration for Micronaut:
jackson:
property-naming-strategy: SNAKE_CASE
serialization-inclusion: always
EDIT:
As I guessed, putting @JsonProperty annotation on each field in record header worked. Exception is gone and all values are populared.
public record User(
long id,
@JsonProperty("is_bot") boolean isBot,
@JsonProperty("first_name") String firstName,
@JsonProperty("user_name") String userName,
@JsonProperty("can_join_groups") boolean canJoinGroups,
@JsonProperty("can_read_all_group_messages") boolean canReadAllGroupMessages,
@JsonProperty("supports_inline_queries") boolean supportsInlineQueries) {
}
However, I don't want to do it for each of my DTO records. How to make jackson.property-naming-strategy: SNAKE_CASE property work?
There are 2 solutions:
public class RecordNamingStrategyPatchModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
context.addValueInstantiators(new ValueInstantiatorsModifier());
super.setupModule(context);
}
/**
* Remove when the following issue is resolved:
* <a href="https://github.com/FasterXML/jackson-databind/issues/2992">Properties naming strategy do not work with Record #2992</a>
*/
private static class ValueInstantiatorsModifier extends ValueInstantiators.Base {
@Override
public ValueInstantiator findValueInstantiator(
DeserializationConfig config, BeanDescription beanDesc, ValueInstantiator defaultInstantiator
) {
if (!beanDesc.getBeanClass().isRecord() || !(defaultInstantiator instanceof StdValueInstantiator) || !defaultInstantiator.canCreateFromObjectWith()) {
return defaultInstantiator;
}
Map<String, BeanPropertyDefinition> map = beanDesc.findProperties().stream().collect(Collectors.toMap(p -> p.getInternalName(), Function.identity()));
SettableBeanProperty[] renamedConstructorArgs = Arrays.stream(defaultInstantiator.getFromObjectArguments(config))
.map(p -> {
BeanPropertyDefinition prop = map.get(p.getName());
return prop != null ? p.withName(prop.getFullName()) : p;
})
.toArray(SettableBeanProperty[]::new);
return new PatchedValueInstantiator((StdValueInstantiator) defaultInstantiator, renamedConstructorArgs);
}
}
private static class PatchedValueInstantiator extends StdValueInstantiator {
protected PatchedValueInstantiator(StdValueInstantiator src, SettableBeanProperty[] constructorArguments) {
super(src);
_constructorArguments = constructorArguments;
}
}
}
You can add the Module to ObjectMapper using
objectMapper.registerModule(new RecordNamingStrategyPatchModule());
It's not the serialization that's the problem, it's the deserialization, if I understand correctly. For deserialization, Jackson needs to read the names of each parameter in the constructor to map the fields correctly from JSON to Java.
One way to do this automatically (without @JsonProperty annotations) is to add the ParameterNamesModule from the jackson-module-parameter-names jar:
mapper.registerModule(new ParameterNamesModule());
This also requires that the classes are compiled with the -parameters compile flag (at least for java 8 - 11). In maven, you do this via:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
I never tested this with Java 16 however, so let me know if it works there, too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With