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

ACCEPT_EMPTY_STRING_AS_NULL_OBJECT does not work with @JsonValue objects #3322

Open
JochemKuijpers opened this issue Nov 10, 2021 · 2 comments

Comments

@JochemKuijpers
Copy link

Bug or feature request: Documentation isn't clear enough to me to make that distinction, so I'll leave the label up to you.

Versions tested: 2.12.4, 2.13.0.

I'm converting some of the IDs in my data payloads to typed IDs instead of simple Strings to improve my code readability and to differentiate the different types of IDs I have in my code base. It seems like having a class with a singular @JsonValue member as below is perfect for that as a drop-in replacement for String values (and there will be identical but differently named classes for the other IDs).

public class TypedId {
	@JsonValue
	public final String id;

	@JsonCreator
	public TypedId(String id) {
		this.id = id;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) { return true; }
		if (o == null || getClass() != o.getClass()) { return false; }
		TypedId typedId = (TypedId) o;
		return Objects.equals(id, typedId.id);
	}

	@Override
	public int hashCode() {
		return Objects.hash(id);
	}
}

Sometimes my ID in JSON are empty strings, and sometimes they are null, due to other-party constraints. I want to deserialize both of those as null instead of TypedId("") or TypedId(null) in the Java code, to avoid the confusing case of having a non-null TypeId object which itself does not have a valid ID.
It seems that Jackson has a DeserializationFeature for this called ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, however it does not seem to work in this use-case.

Here's the containing class for my minimum example demonstrating this behavior:

public class Container {
	@JsonProperty
	public TypedId id;

	// @JsonProperty
	// ... other properties
}

Here are the three test cases I constructed that should succeed according to my expectation, but the second test case fails:

/** Test that non-empty string IDs are correctly deserialized as TypedId classes with the passed ID. */
@Test
public void testNonEmptyStringId() throws JsonProcessingException {
	ObjectMapper objectMapper = new ObjectMapper();
	objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
	Container value = objectMapper.readValue("{\"id\": \"1234-abcd\"}", Container.class);
	assertEquals(new TypedId("1234-abcd"), value.id); // succeeds
}

/** Test that empty strings are treated as null objects, and no TypedId is constructed with the empty string. */
@Test
public void testEmptyStringId() throws JsonProcessingException {
	ObjectMapper objectMapper = new ObjectMapper();
	objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
	Container value = objectMapper.readValue("{\"id\": \"\"}", Container.class);
	assertNull(value.id); // fails, value.id = TypedId(""), not null.
}

/** Test that passing a null value does not construct a TypedId class with a null ID but instead leaves the Container member null. */
@Test
public void testNullId() throws JsonProcessingException {
	ObjectMapper objectMapper = new ObjectMapper();
	objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
	Container value = objectMapper.readValue("{\"id\": null}", Container.class);
	assertNull(value.id); // succeeds
}

The second test fails as Jackson constructs a TypedId class with an empty string despite enabling the deserialization feature to prevent this, which I believe is either a bug, or I would like to request a feature to deal with this specific case.

@JochemKuijpers
Copy link
Author

Sorry for bumping;

Could anyone have a look whether this is a bug, or whether it's a use-case that might require a separate DeserializationFeature?

If a workaround is known for this scenario, that would also be appreciated!
I'm willing to look into fixing the bug or adding the feature myself, if someone can give me some pointers where to look.

@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 16, 2021

This may be due to documentation, but ACCEPT_EMPTY_STRING_AS_NULL_OBJECT does not mean forced coercion from empty String into target type, in cases where there is valid handling already in place.
@JsonValue, for its part, will happily take any String, pass it to @JsonCreator annotated constructor and so on.

So as far as I can see it works as I'd expect to -- I realize this may not be what you'd like to happen.
I would not try to change semantics here for DeserializationFeature.

What you can (and probably need to do) is to create factory method for TypeId, in which you will add special handling for case of empty String. Explicit JSON null will be handled automatically and they will not go via creator method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants