jackson-databind: `@JsonValue` fails for Java Record

Version information 2.12.1 OpenJDK 15.0.1

To Reproduce

Given:

public final record GetLocations(@JsonValue Map<String, URI> nameToLocation)
{
	@JsonCreator
	public GetLocations(Map<String, URI> nameToLocation)
	{
		assertThat(nameToLocation, "nameToLocation").isNotNull();
		this.nameToLocation = new HashMap<>(nameToLocation);
	}
}

I am expecting Jackson to serialize the Map to JSON but instead I get the following exception:

Problem with definition of [AnnotedClass GetLocations]: Multiple 'as-value' properties defined ([field GetLocations#nameToLocation] vs [method GetLocations#nameToLocation()])

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 7
  • Comments: 15 (13 by maintainers)

Commits related to this issue

Most upvoted comments

…though ideologically, I’d prefer to serialize the return value of the getter than the field…

@cowwoc you can remove the @JsonValue annotation from the Record header, and annotate the accessor instead:

public final record GetLocations(Map<String, URI> nameToLocation)
{
	@JsonCreator
	public GetLocations(Map<String, URI> nameToLocation)
	{
		assertThat(nameToLocation, "nameToLocation").isNotNull();
		this.nameToLocation = new HashMap<>(nameToLocation);
	}

	@JsonValue
	@Override
	public Map<String, URI> nameToLocation()
	{
		return nameToLocation;
        }
}

A Trick

Since this issue is caused by (auto-)propagation of annotation on Records components, we learn that the decision to propagate the annotation to either field and/or accessor method is decided by the @Target supported by the annotation itself.

Since @JsonValue can be annotated on ElementType.FIELD & ElementType.METHOD, it gets propagated to both. Knowing this, you can create a custom meta-annotation for @JsonValue that targets only ElementType.METHOD:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@JacksonAnnotationsInside
@JsonValue
public @interface JsonValueAccessor {
}

Then this will then work:

public final record GetLocations(@JsonValueAccessor Map<String, URI> nameToLocation) // Your custom annotation
{
	@JsonCreator
	public GetLocations(Map<String, URI> nameToLocation)
	{
		assertThat(nameToLocation, "nameToLocation").isNotNull();
		this.nameToLocation = new HashMap<>(nameToLocation);
	}
}