Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserializing Java record with @JsonUnwrapped annotation throws exception "can not set final" #3726

Open
jonjanisch opened this issue Jan 9, 2023 · 7 comments
Labels
duplicate Duplicate of an existing (usually earlier) issue Record Issue related to JDK17 java.lang.Record support

Comments

@jonjanisch
Copy link

I'm attempting to deserialize JSON into a simple Java record where one of the members, a simple POJO, is annotated as @JsonUnwrapped.

public record TestRecord(String name, @JsonUnwrapped Address address) {}

If I attempt to deserialize the JSON string {"name":"Bob","city":"New York","state":"NY"}, I get the exception:

    com.fasterxml.jackson.databind.JsonMappingException: Can not set final org.example.AppTest$Address field org.example.AppTest$TestRecord.address to org.example.AppTest$Address
 at [Source: UNKNOWN; byte offset: #UNKNOWN]

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:276)
Caused by: java.lang.IllegalAccessException: Can not set final org.example.AppTest$Address field org.example.AppTest$TestRecord.address to org.example.AppTest$Address
    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:799)

Version information
Tested both 2.13.2 and 2.14.1 - same problem with both versions.

To Reproduce
This uses JDK17 multiline string but trivial to replace with escaped string.

@Test
void should_DeserializeUnwrappedRecord() throws JsonProcessingException {

    Address address = new Address();
    address.setCity("New York");
    address.setState("NY");
    TestRecord expectedRecord = new TestRecord("Bob", address);

    String json = """
            {"name":"Bob","city":"New York","state":"NY"}""";

    JsonMapper objectMapper = new JsonMapper();
    TestRecord actualValue = objectMapper.readValue(json, TestRecord.class);
    assertEquals(expectedRecord, actualValue);
}

Additional context
If I change TestRecord to a plain Java class, it works fine.

I've tried using the annotations @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) and annotating all fields with @JsonProperty to no avail.

@jonjanisch jonjanisch added the to-evaluate Issue that has been received but not yet evaluated label Jan 9, 2023
@cowtowncoder cowtowncoder added Record Issue related to JDK17 java.lang.Record support duplicate Duplicate of an existing (usually earlier) issue labels Jan 14, 2023
@cowtowncoder cowtowncoder removed the to-evaluate Issue that has been received but not yet evaluated label Jan 23, 2023
@Taz03
Copy link

Taz03 commented Mar 1, 2023

@cowtowncoder is there any updates on this?

@pjfanning
Copy link
Member

pjfanning commented Mar 1, 2023

@Taz03 come on - your comments go to lots of people. Do you really need to spam everyone every 3 hours?

@cowtowncoder
Copy link
Member

@JsonUnwrapped does not work with Constructor-based deserialization. There is an earlier issue #1467 that covers it.

@Taz03 If there was an update, it would be noted here. There isn't, hasn't been, and no immediate progress in sight.

@zjwmiao
Copy link

zjwmiao commented Apr 27, 2023

you can customize a deserilaizer

@JsonDeserialize(using = TestDeserializer.class)
public record TestRecord(String name, @JsonUnwrapped Address address) {}

static class TestDeserializer extends JsonDeserializer<TestRecord> {
  @Override
  public TestRecord deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JacksonException {
    JsonNode node = jsonParser.getCodec().readTree(jsonParser);
    String name = node.get("name").asText();
    String city = node.get("city").asText();
    String state = node.get("state").asText();
    return new TestRecord(name, new Address(city, state));
  }
}
ObjectMapper objectMapper = new ObjectMapper();
String json = "{\"name\":\"Bob\",\"city\":\"New York\",\"state\":\"NY\"}";
System.out.println(objectMapper.readValue(json, TestRecord.class));
// TestRecord[name=Bob, address=Address[city=New York, state=NY]]

Or, use @JsonCreator and @JsonProperty

public record TestRecord(String name, @JsonUnwrapped @JsonProperty(access = Access.READ_ONLY) Address address) {
  @JsonCreator
  public TestRecord(@JsonProperty("name") String name, @JsonProperty("city") String city,
      @JsonProperty("state") String state) {
    this(name, new Address(city, state));
  }
}
ObjectMapper objectMapper = new ObjectMapper();
String json = "{\"name\":\"Bob\",\"city\":\"New York\",\"state\":\"NY\"}";
System.out.println(objectMapper.readValue(json, TestRecord.class));
// TestRecord[name=Bob, address=Address[city=New York, state=NY]]

nkonev added a commit to nkonev/videochat that referenced this issue Nov 23, 2023
@Eitraz
Copy link

Eitraz commented Oct 18, 2024

As I spent quite some time to find a solution that fits my case, it's not more than right to share it here.
In my case the "address" is a more complex object and handing the fields manually was not an option for me.

This is similar to what I ended up with, based on the example record above:

public record TestRecord(
        String name,
        @JsonUnwrapped Address address
) {
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public TestRecord(JsonNode node) {
        this(
                ObjectMapperHolder.getObjectMapper().convertValue(node.get("name"), String.class),
                ObjectMapperHolder.getObjectMapper().convertValue(node, Address.class)
        );
    }
}

@cowtowncoder
Copy link
Member

Actually there might be way forward for this, see #4271.

@uukoo
Copy link

uukoo commented Nov 8, 2024

Just use builder as deserializer will fix this.

@Builder
@Jacksonized
public record TessRecord (
    String name,
   @JsonUnwrapped
    Address address
) {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate Duplicate of an existing (usually earlier) issue Record Issue related to JDK17 java.lang.Record support
Projects
None yet
Development

No branches or pull requests

7 participants