skip to Main Content

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?

2

Answers


  1. 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.

    Login or Signup to reply.
  2. There are 2 solutions:

    1. Wait until Jackson fixes this issue, as of 2.13.0-rc02 this issue is still not resolved.
    2. Use solution proposed by GitHub user mentioned in the comments of the above issue.
    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());
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search