How to prevent PatchUpdateException on json deserialization? - java

I'm parsing json with custom desereailizer. It reads JsonNode and then deserializes entity from json by ObjectMapper.
public static class CustomDeserializer extends StdDeserializer<Custom> implements ResolvableDeserializer {
private final JsonDeserializer<?> defaultDeserializer;
public CustomDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(Custom.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Custom deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final ObjectNode node = jp.readValueAsTree();
// some node structure validation
try (final TreeTraversingParser newJsonParser = new TreeTraversingParser(node)) {
newJsonParser.nextToken();
return (Custom) defaultDeserializer.deserialize(newJsonParser, ctxt);
}
}
#Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
}
On some requests it works, on other throws
com.fasterxml.jackson.core.json.UTF8StreamJsonParser cannot be cast to com.fasterxml.jackson.databind.node.TreeTraversingParser
or
m.fasterxml.jackson.core.json.ReaderBasedJsonParser cannot be cast to com.fasterxml.jackson.databind.node.TreeTraversingParser
How to prevent that case?

Related

How to convert JSON object to a Pair object in JAVA

From the API client, I am sending
{
"scheduledDateTime": "27-12-2020 08:55:46",
"subscriptionClosedBy": "30-12-2020 08:55:46",
"presenter":{"first":"Jone Doe","second":"Senior Tech Lead"}
}
JSON object to my following API endpoint.
public ResponseEntity<ApiResponse> saveWorkshop(#RequestBody Workshop workshop)
Workshop class contains a Pair type attribute as follows.
public class Workshop extends PersistObject {
private LocalDateTime scheduledDateTime;
private LocalDateTime subscriptionClosedBy;
private Pair<String, String> presenter;
}
When the request hits the endpoint exception is thrown as follows.
Caused by:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of org.springframework.data.util.Pair
How can I overcome this issue?
I created the following custom deserializer class
public final class PairDeserializer extends JsonDeserializer<Pair<String, String>> {
static private ObjectMapper objectMapper = null;
#Override
public Pair<String, String> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser);
String first = objectMapper.treeToValue(treeNode.get("first"), String.class);
String second = objectMapper.treeToValue(treeNode.get("second"), String.class);
return Pair.of(first, second);
}
}
and applied it to the Workshop class as follows
#JsonDeserialize(using = PairDeserializer.class)
private Pair<String, String> presenter;
Then the issue was solved.

Java Jackson: why do I get JsonMappingException although I used #JsonDeserialize

I need some help with Jackson library in Java. I have the following class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class MarketHistoryData {
private String countryCode;
#JsonDeserialize(keyUsing = BpTimeDeserializer.class)
private Map<BpTime, Double> hourToExpectedEngagePaidListings;
#JsonDeserialize(keyUsing = BpTimeDeserializer.class)
private Map<BpTime, Double> hourMinuteToExpectedEngagePaidListings;
public MarketHistoryData() {}
...
// getters and setters
}
I understood that Jackson has difficulties with deserializing a map which its keys are an object. Therefore, I added the annotations:
#JsonDeserialize(keyUsing = BpTimeDeserializer.class)
The class BpTimeDeserializer is:
public class BpTimeDeserializer extends KeyDeserializer {
private ObjectMapper mapper = new ObjectMapper();
#Override
public BpTime deserializeKey(String key, DeserializationContext ctxt) throws IOException {
return mapper.readValue(key, BpTime.class);
}
}
Still, I get an error during the deserialization process:
*****.UncheckedExecutionException:
com.fasterxml.jackson.databind.JsonMappingException: Unrecognized
token 'com': was expecting (JSON String, Number, Array, Object or
token 'null', 'true' or 'false') at [Source:
(String)"****BpTime#5922062e[hour=1,minute=0]";
line: 1, column: 1] (through reference chain:
***PacerCycleOutput["campaignIdToFactorComputationData"]->java.util.LinkedHashMap["1011620661"]->****FactorComputationData["selectedMarketHistoryDataForCampaign"]->***MarketHistoryData["hourToExpectedEngagePaidListings"])
Do you know what I can do to overcome this error?
I finally found a way how to resolve this error.
#JsonIgnoreProperties(ignoreUnknown = true)
public class MarketHistoryData {
private String countryCode;
#JsonDeserialize(keyUsing = BpTimeDeserializer.class)
#JsonSerialize(keyUsing = BpTimeSerializer.class)
private Map<BpTime, Double> hourToExpectedEngagePaidListings;
#JsonDeserialize(keyUsing = BpTimeDeserializer.class)
#JsonSerialize(keyUsing = BpTimeSerializer.class)
private Map<BpTime, Double> hourMinuteToExpectedEngagePaidListings;
...
}
public class BpTimeDeserializer extends KeyDeserializer {
private ObjectMapper mapper = new ObjectMapper();
#Override
public BpTime deserializeKey(String key, DeserializationContext ctxt) throws IOException {
String[] keySplit = key.split(" ");
BpTime bpTime = new BpTime();
if (!keySplit[0].equals("-1")) {
bpTime.setHour(Integer.parseInt(keySplit[0]));
}
if (!keySplit[1].equals("-1")) {
bpTime.setMinute(Integer.parseInt(keySplit[1]));
}
return bpTime;
}
}
public class BpTimeSerializer extends StdSerializer<BpTime> {
public BpTimeSerializer() {
this(null);
}
public BpTimeSerializer(Class<BpTime> t) {
super(t);
}
#Override
public void serialize(BpTime value, JsonGenerator generator, SerializerProvider arg2) throws IOException {
generator.writeFieldName(String.format("%s %s", ofNullable(value.getHour()).orElse(-1),
ofNullable(value.getMinute()).orElse(-1)));
}
}

Java serializer/deserializer for List<Long>

I need to make serializer and deserializer, because I get this error:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.Long` out of START_ARRAY token
And I make it:
public class CalculatorIDsDeserializer extends JsonDeserializer<Long> {
#Override
public Long deserialize(final JsonParser jsonParser,
final DeserializationContext deserializationContext)
throws IOException {
String dataString = jsonParser.getText();
return Long.parseLong(dataString);
}
}
and
public class CalculatorIDsSerializer extends JsonSerializer<Long>{
#Override
public void serialize(Long aLong, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(Long.toString(aLong));
}
}
but now I have this error:
com.fasterxml.jackson.databind.JsonMappingException: org.hibernate.collection.internal.PersistentBag cannot be cast to java.lang.Long
And I make it for:
#JsonDeserialize(using = CalculatorIDsDeserializer.class)
#JsonSerialize(using = CalculatorIDsSerializer.class)
private List<Long> insuranceFileIDs = new ArrayList<>();
Some tips how to solve it? I have no more ideas after a few hours. Thaks
Update: I change it to this, but I get same error :(
public class CalculatorIDsDeserializer extends JsonDeserializer<List<Long>> {
#Override
public List<Long> deserialize(final JsonParser jsonParser,
final DeserializationContext deserializationContext)
throws IOException {
String dataString = jsonParser.getText();
List<Long> ids = null;
ids.add(Long.parseLong(dataString));
return ids;
}
}
Update2:
public class CalculatorIDsSerializer extends JsonSerializer<List<Long>>{
#Override
public void serialize(List<Long> longs, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
longs.forEach(item ->{
try {
jsonGenerator.writeString(Long.toString(item));
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
and
public class CalculatorIDsDeserializer extends JsonDeserializer<List<Long>> {
#Override
public List<Long> deserialize(final JsonParser jsonParser,
final DeserializationContext deserializationContext)
throws IOException {
String dataString = jsonParser.getText();
List<Long> ids = new ArrayList<>();
ids.add(Long.parseLong(dataString));
return ids;
}
}
And i get: Problem deserializing property 'insuranceFileIDs' (expected type: [simple type, class java.lang.Long]; actual type: java.util.ArrayList), problem: argument type mismatch

Generic Enum Json Deserialization

I needed a better hibernate enum mapping and this page served me well (Except I used char type instead of int).
Next question is how can I serialize/deserialize an enum in a generic way?
Think of a Gender enum:
#JsonSerialize(using = PersistentEnumSerializer.class)
#JsonDeserialize(using = PersistentEnumDeserializer.class)
public enum Gender implements PersistentEnum {
MALE("M", "Male"), FEMALE("F", "Female");
private String code;
private String display;
Gender(String code, String display) {
this.code = code;
this.display = display;
}
public String getName() {
return name();
}
public String getCode() {
return code;
}
public String getDisplay() {
return display;
}
public String toString() {
return display;
}
}
which implements getName(), getCode() and getDisplay() methods of PersistentEnum interface. Serializing is easy:
public class PersistentEnumSerializer extends JsonSerializer<PersistentEnum> {
#Override
public void serialize(PersistentEnum object, JsonGenerator generator, SerializerProvider provider) throws IOException, JsonProcessingException {
generator.writeStartObject();
generator.writeFieldName("name");
generator.writeString(object.getName());
generator.writeFieldName("code");
generator.writeString(object.getCode());
generator.writeFieldName("display");
generator.writeString(object.getDisplay());
generator.writeEndObject();
}
}
but how can I deserialize in java 6? In java 8, I would add a static method to PersistentEnum interface.
public class PersistentEnumDeserializer extends JsonDeserializer<PersistentEnum> {
#Override
public PersistentEnum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
//String value = node.get("name").asText();
//TODO Somehow I need to get Gender.MALE if the json is {"name":"MALE","code":"M","display":"Male"}
return null;
}
}
One possible solution is to add a new method getType() to PersistentEnum which will identify the type of Enum.
#JsonSerialize(using = PersistentEnumSerializer.class)
#JsonDeserialize(using = PersistentEnumDeserializer.class)
public enum Gender implements PersistentEnum {
#Override
public String getType() {
return "gender";
}
}
Serializer should also be modified to include type while serialization.
public class PersistentEnumSerializer extends JsonSerializer<PersistentEnum> {
#Override
public void serialize(PersistentEnum object, JsonGenerator generator, SerializerProvider provider) throws IOException, JsonProcessingException {
generator.writeStartObject();
generator.writeFieldName("name");
generator.writeString(object.getName());
generator.writeFieldName("code");
generator.writeString(object.getCode());
generator.writeFieldName("display");
generator.writeString(object.getDisplay());
generator.writeFieldName("type");
generator.writeString(object.getType());
generator.writeEndObject();
}
}
Deserializer can be written as shown below.
public class PersistentEnumDeserializer extends JsonDeserializer<PersistentEnum> {
#Override
public PersistentEnum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return findEnum(node.get("type").asText(), node.get("name").asText());
}
private PersistentEnum findEnum(String type, String name) {
switch (type) {
case "gender":
return Gender.valueOf(name);
// handle other types here.
default:
return null;
}
}
}
While #Justin Jose's solution is not the one I'm looking for (because for each enum we need to add to findEnum method), it gave me a good hint.
If getType is implemented like this:
#Override
public String getType() {
return getClass().getSimpleName();
}
and findEnum like this
private PersistentEnum findEnum(String type, String name) {
Class<?> c = null;
try {
c = Class.forName("enums." + type); //Assuming all PersistentEnum's are in "enums" package
if (PersistentEnum.class.isAssignableFrom(c)) {
Method method = c.getMethod("name");
for (Object object : c.getEnumConstants()) {
Object enumName = method.invoke(object);
if (name.equals(enumName))
return (PersistentEnum) object;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
it may work. Not tested and possibly vulnerable.

Exclude custom deserializer with jackson

In my pojo class I have configured a CustomDeserializer with annotation
#JsonDeserialize(using = CustomDeserializer.class)
class Myclass {
private String A;
#JsonIgnore
private String B;
#JsonIgnore
private String C;
private String D;
...
private String Z;
/*getters and setters*/
}
In CustomDeserializer, I want to manage only some of the fields and leave the rest for Jackson to manage.
CustomDeserializer.java
public class CustomDeserializer extends StdDeserializer<Myclass > {
private static final long serialVersionUID = 4781685606089836048L;
public CustomDeserializer() {
super(Myclass.class);
}
#Override
public Myclass deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, IllegalResponseException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Myclass myClass = mapper.readValue(root.toString(), Myclass.class);
//--- HERE MANAGE FIELD B ---
myClass.setB(myNewB);
//--- HERE MANAGE FIELD C ---
myClass.setC(myNewC);
return myClass;
}
}
This way I run into an infinite loop because of the following line:
mapper.readValue(root.toString(), Myclass.class);
Is there a way to set default behavior when using Jackson so that I can exclude my CustomDeserializer?
The problem is that you will need a fully constructed default deserializer; and this requires that one gets built, and then your deserializer gets access to it. DeserializationContext is not something you should either create or change; it will be provided by ObjectMapper.
To meet your requirement you can start by writing a BeanDeserializerModifier and registering it via SimpleModule.
The following example should work:
public class CustomDeserializer extends StdDeserializer<Myclass> implements ResolvableDeserializer
{
private static final long serialVersionUID = 7923585097068641765L;
private final JsonDeserializer<?> defaultDeserializer;
public CustomDeserializer (JsonDeserializer<?> defaultDeserializer)
{
super(Myclass.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override public Myclass deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
Myclass deserializedMyclass = (Myclass) defaultDeserializer.deserialize(jp, ctxt);
// custom logic
return deserializedMyclass;
}
// You have to implement ResolvableDeserializer when modifying BeanDeserializer
// otherwise deserializing throws JsonMappingException
#Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
{
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
{
SimpleModule module = new SimpleModule();
//Writing a new BeanDeserializerModifier
module.setDeserializerModifier(new BeanDeserializerModifier()
{
#Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
{
if (beanDesc.getBeanClass() == Myclass.class)
return new CustomDeserializer(deserializer);
return deserializer;
}
});
//register the BeanDeserializerModifier via SimpleModule
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Myclass myclass = mapper.readValue(new File("d:\\test.json"), Myclass.class);
}
}

Categories