Search code examples
javajsonjacksondeserializationrecord

Jackson deserialization into Record with List runs into SetterlessProperty issue


QUESTION: How do I capture the (optional) List field media_links_txt from the received JSON as either null or its actual value with concise/idiomatic modern Java?

Functional wishes are:

  • Some properties have explicit @JsonGetter and @JsonSetter fieldnames since the Record is used both as deserialization target and response model (API clients demand different field names as incoming JSON)
  • The optionalStuff field is ... well optionally present in deserialization flow

Code is as follows:

import com.fasterxml.jackson.annotation.*;
import org.springframework.lang.NonNull;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public record TestResponse(@NonNull Response response) {

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Response(@NonNull List<Doc> docs,
                           @NonNull Integer numFound,
                           @NonNull Integer start) {

        public record Doc(@NonNull String id,
                              @NonNull @JsonGetter("content_t") @JsonSetter("content") String content,
                              @NonNull @JsonGetter("title_t") @JsonSetter("title") String title,
                              @JsonGetter("media_links_txt") @JsonSetter("mediaLinks") List<String> mediaLinks) {
        }
    }
}

When this code receives JSON that looks like this:

{
  "response" : {
    "docs" : [ {
      "content_t" : "Test content 1",
      "title_t" : "Test title 1",
      "media_links_txt" : [ "d7810883-42de-4280-a286-5bb14e517ce3", "fa60db0a-0e28-4c9c-8092-4eafe57251cd" ],
      "id" : "54870dc265c855c516bcd0a2f93f2267"
    }, {
      "content_t" : "Test content 2",
      "title_t" : "Test content 2",
      "id" : "a4a38aa64427d63aab0c4412eb659586"
    } ],
    "numFound" : 2,
    "start" : 0
}

It errors with the following message: Should never call set() on setterless property and points towards media_links_txt. This confuses me ...

  • Records don't have setters to begin with so it tries to use the constructor, which I am currently thinking is fine and desired (it can be assigned a null value for mediaLinks is what I think I have written)
  • But it can construct the docs field (also a List) without problem?

So I tried to trick it with the following code (maybe it required a non-default type)

import com.fasterxml.jackson.annotation.*;
import org.springframework.lang.NonNull;

import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public record TestResponse(@NonNull Response response) {

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Response(@NonNull List<Doc> docs,
                           @NonNull Integer numFound,
                           @NonNull Integer start) {

        public record Doc(@NonNull String id,
                              @NonNull @JsonGetter("content_t") @JsonSetter("content") String content,
                              @NonNull @JsonGetter("title_t") @JsonSetter("title") String title,
                              @JsonGetter("media_links_txt") @JsonSetter("mediaLinks") List<StringClone> mediaLinks) {

            public record StringClone(@NonNull String mediaLinksRetry) {
            }
        }
    }
}

Which throws: Cannot construct instance of StringClone (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('d7810883-42de-4280-a286-5bb14e517ce3')

  • Wait ... there is no String-argument constructor on a String-only Record? I am fairly sure there should be, but I might be missing something and would like to learn so!

QUESTION: How do I capture the (optional) List field media_links_txt from the received JSON as either null or its actual value with concise/idiomatic modern Java?

Used versions are:

  • jackson-databind 2.13.5
  • spring framework 5.3.27 (via spring boot starter 2.7.11)

I think https://github.com/FasterXML/jackson-databind/issues/2692 might describe the issue, but no proper workaround as of yet.


Solution

  • The value parameter in your JsonSetters needs to match the field name in the JSON. I assume Jackson recognises that the type is a record and is able to call the constructor if the ctor parameter names line up with the JSON field names.

    I.e., f you change the JsonSetter annotations like this:

     public record Doc(String id,
                      @JsonGetter("content_t") @JsonSetter("content_t") String content,
                      @JsonGetter("title_t") @JsonSetter("title_t") String title,
                      @JsonGetter("media_links_txt") @JsonSetter("media_links_txt") List<String> mediaLinks) {
    }
    

    then it seems to work, at least it does for me. This can be further simplified by replacing the JsonGetter/JsonSetter pairs with a single JsonProperty annotation:

    public record Doc(String id,
                      @JsonProperty("content_t") String content,
                      @JsonProperty("title_t") String title,
                      @JsonProperty("media_links_txt") List<String> mediaLinks) {
    }