Deserialising problem with lombok builder and jackson, cannot construct instance of [...] no String-argument constructor/factory method to deserialize - spring-boot

This is a simplified version of a problem I've been having. Given these classes:
#Value
#Jacksonized
#Builder(builderClassName = "Builder", setterPrefix = "with")
public class Limits {
Limit minimum;
Limit maximum;
}
#Value
#Jacksonized
#Builder(builderClassName = "Builder", setterPrefix = "with")
public class Limit {
#JsonValue
String value;
}
and this code:
Limits limits = Limits.builder()
.withMinimum(Limit.builder().withValue("One-Day").build())
.withMaximum(Limit.builder().withValue("One-Year").build())
.build();
System.out.println(objectMapper.writeValueAsString(limits));
it works as expected and gives me the following output:
{
"minimum": "One-Day",
"maximum": "One-Year"
}
However, when I try to deserialise the same JSON string, as follows:
String json = """
{"minimum":"One-Day","maximum":"One-Year"}
""";
objectMapper.readValue(json, Limits.class);
I get the following error:
Cannot construct instance of `Limit$Builder` (although at least one Creator exists):
no String-argument constructor/factory method to deserialize from String value ('One-Day')
at [Source: (String)"{"minimum":"One-Day","maximum":"One-Year"}"; line: 1, column: 12]
Is there a way to make it work without changing the data model or the JSON?
I tried adding #JsonCreator to the Builder of Limit as follows, but gives the same error
#Value
#Jacksonized
#Builder(builderClassName = "Builder", setterPrefix = "with")
public class Limit {
#JsonValue
String value;
public static final class Builder {
#JsonCreator
public Builder withValue(String value) {
this.value = value;
return this;
}
}
}
Appreciate any input on what I might be missing here.

Jackson does not support #JsonCreator in combination with builders. #JsonCreator basically says: The annotated method constructs a new instance, passing the JSON value as argument to its single parameter. That does not work for builders, because there is no such method that does it both (there are two separate methods, one for receiving a JSON value, and one for constructing the instance).
However, there is a simple workaround in your case without loosing immutability. Just put #JsonCreator on an all-args constructor as follows:
#Value
#AllArgsConstructor(onConstructor_ = {#JsonCreator})
public class Limit {
#JsonValue
String value;
}
You can keep the #Builder if you want to, but there is no need to do so.

Related

Java jackson deserialize json - Getting symbol "=" instead of ":"

This is what I have. I'm trying to deserialize a json but it doesn't work:
public class MyClass {
private Object attribute;
#JsonCreator
public MyClass(Object attribute) {
this.attribute = attribute;
}
#JsonProperty("attr")
public Object getAttribute() {
return attribute;
}
}
public void method() {
InputStream eventsStream = this.getClass().getClassLoader()
.getResourceAsStream("fileName.json");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.readValue(eventsStream, MyClass.class);
}
fileName.json
{
"value1": 1,
"value2": [
{
"subValue1": "valueExample"
}
]
}
I'm getting this result with symbol "=" instead of ":":
{value1=1, value2=[{subValue=valueExample}]}
It must be a property or something like that. Any idea? Thanks
Why don't you save the deserialization into a variable?
MyClass myClassObject = objectMapper.readValue(eventsStream, MyClass.class);
if you do this, you will have direct the deserialized object in myClassObject and so you can use it easily.
Another method to deserialize would be to do it using Gson like:
new Gson().fromJson(eventStream.toString(), MyClass.class); //you need the eventStream as a String type
the import is this one:
import com.google.gson.Gson;
Can you please tell us why do you need to print the deserialized value?

Spring Jackson ObjectMapper Fails to JSON Deserialize While the Unit Test Passes

I've got a class in my Spring web application:
#Value // or #Data Lombok
public class Bar {
private final BigDecimal value;
#JsonCreator
public Bar(double value) {
this.value = BigDecimal.valueOf(value);
}
}
I wrote a unit test which passes:
#Test
void test() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Bar bar = new Bar(12.34);
assertEquals(mapper.readValue("12.34", Bar.class), bar);
}
But when I send a POST to the controller, it fails to deserialize the request body (which is just 12.34 to be deserialized to a Bar instance) with the following error:
JSON parse error: Cannot construct instance of com.example.demo.Bar (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (12.34); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of com.example.demo.Bar (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (12.34)
If I remove the #Value, it can deserialize it. To make it even more confusing, if I add the constructor (created by #Value) manually, still it works. But if I remove #JsonCreator it again can deserialize it.
What am I missing?
#Jsoncreator should be used in conjunction with #JsonProperty, to specify how to deserialize a JSON object. So if for example you have a rest controller that some JSON like:
{
"value": 123
}
Your constructor should be annotated like such:
#JsonCreator
public Bar(#JsonProperty("value") double value) {
this.value = BigDecimal.valueOf(value);
}
Although this might see redundant, the idea is that this allows for more flexibily in cases were the JSON you intend to deserialize doesn't match the name of your properties.
So for example if the object you are receiving still has a key value, but your class has a property myValue, the following will work:
public class Example {
private final BigDecimal myValue;
#JsonCreator
public Bar(#JsonProperty("value") double myValue) {
this.myValue= BigDecimal.valueOf(myValue);
}
}

Is it possible to drive the #Size "max" value from a properties file?

I'm using Spring 3.1.1.RELEASE. I have a model with the following attribute
import javax.validation.constraints.Size;
#Size(max=15)
private String name;
I validate the model in my controller my running
#RequestMapping(value = "/save", method = RequestMethod.POST)
public ModelAndView save(final Model model,
#Valid final MyForm myForm,
I would like to have the "15" value come from a properties file instead of hard-coded, but am unclear if that's possible or how its done. Any ideas?
This is not possible. The constant expression that you provide as a value for the max attribute is added at compile time. There is no way to change the value of an annotation at runtime. Setting it from a properties file you read is therefore not possible
What you can do instead is to create and register your own Validator for the your class that has that field. For example,
public class MyValidator implements Validator {
public void validate(Object target, Errors errors) {
MyObject obj = (MyObject) target;
int length = getProperties().get("max.size");
if (obj.name.length() > length) {
errors.rejectValue("name", "String length is bigger than " + length);
}
}
public boolean supports(Class<?> clazz) {
return clazz == MyOBject.class;
}
}
Take a look at Spring's validation framework.

Spring Data Neo4j: Converter of object to string works, but object to long is not executed

I have a really strange issue with converting from domain objects to those Neo4j can natively store as property value. As a test case I use Joda's DateTime. A object of that type can be converted to a String or Long quite easily.
The conversion from DateTime to String works flawlessly with this code:
public class DateTimeToStringConverter implements Converter<DateTime, String> {
#Override
public String convert(DateTime source) {
return source.toDateTimeISO().toString();
}
}
The property shows up in the node:
Node[1] {
'__type__' = '...',
'entityEditedAt' = '2012-12-28T12:32:50.308+01:00',
'entityCreatedAt' = '2012-12-28T12:32:50.297+01:00',
...
}
However if I like to save the DateTime as Long (useful to sort by time in Cypher), it does not work at all. Here is my converter:
public class DateTimeToLongConverter implements Converter<DateTime, Long> {
#Override
public Long convert(DateTime source) {
return source.toDateTimeISO().getMillis();
}
}
The property is not saved on the node. Thus it is missing completely. No exception is thrown. It seems like the conversion code is not called at all.
The converters are hooked to Spring Data using code based configuration:
#Bean
public ConversionServiceFactoryBean conversionService() {
Set converters = Sets.newHashSet();
// These work!
converters.add(new DateTimeToStringConverter());
converters.add(new StringToDateTimeConverter());
// These don't :-(
//converters.add(new DateTimeToLongConverter());
//converters.add(new LongToDateTimeConverter());
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
bean.setConverters(converters);
return bean;
}
Any clues? I'm quite lost here, as it should work in my opinion...
Edit
I found following text in the Spring Data Neo4j documentation:
All fields convertible to a String using the Spring conversion services will be stored as a string.
Does this mean, that only conversions to string are supported? This seems rather limiting.
Tell SDN that you want to store your joda DateTime property as a long with:
#NodeEntity
public class MyEntity {
...
#GraphProperty(propertyType = Long.class)
private DateTime timestamp;
....
}
Then your registered DateTimeToLongConverter will kick in.

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Resources