Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why record class can't be properly deserialized with Jackson?

Tags:

java

jackson

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?

like image 964
Praytic Avatar asked Dec 05 '25 14:12

Praytic


2 Answers

There are 2 solutions:

  1. Upgrade to Jackson 2.15.0, which fixes this issue.
  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());
like image 78
Tuby Avatar answered Dec 07 '25 03:12

Tuby


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.

like image 38
GeertPt Avatar answered Dec 07 '25 05:12

GeertPt