jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

`MapperFeature` to ignore setter/getter method implicit detection for Record types

Open garretwilson opened this issue 2 years ago • 12 comments

Describe your Issue

Because Java is not Delphi ObjectPascal or modern JavaScript or Kotlin, it has no idea of "properties", although the JavaBeans specification was an attempt to shoehorn in the concept by guessing based upon convention. That's why in the wild olden days, Jackson assumed that a method String getDisplayName() was a getter; and if there were no setDisplayName(String), well, it's a getter to an imputed read-only property. If you didn't like it, you added a @JsonIgnore annotation to your class. (Back then we had a heck of a time figuring out how to make an immutable instance if the constructor had String foo, String bar, …, too.)

But now we have the Java record, which (despite its downsides—it's just one step to a lot of other features coming in Java) beautifully encapsulates not only a set of fields, but also the names of the fields and their required order in the constructor! Moreover Java adds some nifty new reflection methods to access records at runtime.

So let's say we have this record:

/**
 * A user's profile information.
 * @param username The user's login identifier.
 * @param firstName The first name of the user.
 * @param lastName The last name of the user.
 */
public record UserProfile(@Nonnull String username, @Nonnull String firstName, @Nonnull String lastName) {

  public UserProfile {
    requireNonNull(username);
    requireNonNull(firstName);
    requireNonNull(lastName);
  }

  /** @return A form of the user name appropriate for displaying in messages. */
  public String getDisplayName() {
    return firstName() + " " + lastName();
  }

}

We know at runtime exactly what the constructor types and names (e.g. username); and which fields they correspond to. We know what the field getters are (e.g. username(). More importantly, we know what methods are not getters, and have nothing to do with the fields—getDisplayName() is an example.

Unfortunately Jackson will generate a JSON object that has a displayName attribute, which will prevent the object from being parsed back round-trip, because displayName doesn't correspond to any of the record fields.

Maybe the user wanted to generate a JSON object with the displayName attribute and never use it for deserializing back to a UserProfile instance. That's a valid use case, but it doesn't seem like it should be the default use case.

I can get around this by adding a @JsonIgnore:

  /** @return A form of the user name appropriate for displaying in messages. */
  @JsonIgnore
  public String getDisplayName() {
    return firstName() + " " + lastName();
  }

But I don't want to dirty the model (even though it's a DTO model) with serialization information unless I have to. As recounted above, at one time we had to. But with Java record, we shouldn't have to.

Is there a way to configure Jackson to automatically ignore non-field methods for Java record? If there is something I could use in JsonMapper.builder() that would be fine. Is there something like JsonMapper.builder().serializationMethodInclusion(JsonInclude.Include.NON_RECORD)? If not, what would you recommend as the best way forward to get this sort of functionality? Is there some little logic I can inject into my JsonMapper to detect and handle this case, for example?

garretwilson avatar Oct 12 '23 21:10 garretwilson