Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JsonDeserializer does not work on the Class but only on the single element of the class

I created a new Deserializer to be able to make empty strings be written as null

public class CustomDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException  {
        JsonNode node = jsonParser.readValueAsTree();
        if (node.asText().isEmpty()) {
            return null;
        }
        return node.toString();
    }
}

Trying to make the single annotation on each User field, the Custom works but by inserting the annotation on the whole class, I can no longer print the Json message

@JsonDeserialize(using = CustomDeserializer.class)
public class User {
    private String firstName;
    private String lastName;
    private String age;
    private String address; }

The CustomExceptionHandler throws me this error :Class MethodArgumentNotValidException This is my Kafka Consumer, the only one where I have entered a validation annotation, but even removing it gives me the same error

public class KafkaConsumer {

    @Autowired
    private UserRepository userRepository;

    @KafkaListener(topics = "${spring.kafka.topic.name}")
    public void listen(@Validated User user) {

        User  user = new User(user);
        UserRepository.save(user.getName(), user);
    }
}

ObjectMapper

public ObjectMapper getObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.setSerializationInclusion(Include.NON_NULL);
    return mapper;
}

Is it possible to make it work across the whole class?

like image 614
Jacket Avatar asked Mar 12 '21 10:03

Jacket


People also ask

What is deserialize in JSON?

Deserialize (String, Type, JsonSerializerOptions) Parses the text representing a single JSON value into an instance of a specified type.

How to use jsonserializer with Gson?

The JsonSerializer interface looks like this: After creating custom serializer for Json, we will also need to register this serializer through GsonBuilder.registerTypeAdapter (Type, Object). Gson invokes it’s call-back method serialize () during serialization when it encounters a field of the specified type.

How to deserialize class problemdetails in MVC?

The deserialization of class ProblemDetails always return NULL value for properties (Title, status, etc.) create a MVC application with .net core 3.0, try to deserialize below JSON string in Index () action. This JSON string was created by system.text.json.jsonserialize.serialize.

How to register a custom Serializer for a JSON object?

JsonSerializer interface The JsonSerializer interface looks like this: After creating custom serializer for Json, we will also need to register this serializer through GsonBuilder.registerTypeAdapter (Type, Object).


2 Answers

If you want an empty String representing the whole object to be treated as null, you can enable the ACCEPT_EMPTY_STRING_AS_NULL_OBJECT Jackson deserialization feature, disabled by default.

You can include it when configuring your ObjectMapper:

public ObjectMapper getObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.setSerializationInclusion(Include.NON_NULL);
    // Enable ACCEPT_EMPTY_STRING_AS_NULL_OBJECT deserialization feature
    mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
    return mapper;
}

As abovementioned, it is useful when you want to treat an empty String representing the whole object as null; however, it will not work for individual properties of type String: in the later case you can safely use your custom deserializer, so, the solution is in fact a mix of both approaches, use the ACCEPT_EMPTY_STRING_AS_NULL_OBJECT deserialization feature to deal with the whole object, and your custom deserializer for handling individual String properties.

Please, see this and this other related SO questions.

You can improve your custom User deserializer as well. Please, consider for example (I refactored the name to UserDeserializer for clarity):

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;

public class UserDeserializer extends JsonDeserializer<User> {

  @Override
  public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
    JsonNode node = jsonParser.readValueAsTree();
    Iterator<String> fieldNames = node.fieldNames();
    // Process Jackson annotations looking for aliases
    Map<String, String> fieldAliases = this.getAliases();
    User user = new User();
    boolean anyNonNull = false;
    // Iterate over every field. The deserialization process assume simple properties
    while(fieldNames.hasNext()) {
      String fieldName = fieldNames.next();
      JsonNode fieldValue = node.get(fieldName);
      String fieldValueTextRepresentation = fieldValue.asText();
      if (fieldValueTextRepresentation != null && !fieldValueTextRepresentation.trim().isEmpty()) {
        // Check if the field is aliased
        String actualFieldName = fieldAliases.get(fieldName);
        if (actualFieldName == null) {
          actualFieldName = fieldName;
        }

        this.setFieldValue(user, actualFieldName, fieldValueTextRepresentation);
        anyNonNull = true;
      }
    }

    return anyNonNull ? user : null;
  }

  // Set field value via Reflection
  private void setFieldValue(User user, String fieldName, String fieldValueTextRepresentation) {
    try {
      Field field = User.class.getDeclaredField(fieldName);
      Object fieldValue = null;
      Class clazz = field.getType();
      // Handle each class type: probably this code can be improved, but it is extensible and adaptable,
      // you can include as many cases as you need.
      if (clazz.isAssignableFrom(String.class)) {
        fieldValue = fieldValueTextRepresentation;
      } else if (clazz.isAssignableFrom(LocalDate.class)) {
        // Adjust the date pattern as required
        // For example, if you are receiving the information
        // like this: year-month-day, as in the provided example,
        // you can use the following pattern
        fieldValue = LocalDate.parse(fieldValueTextRepresentation, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
      } else if (clazz.isAssignableFrom(Integer.class)) {
        fieldValue = Integer.parseInt(fieldValueTextRepresentation);
      }
      field.setAccessible(true);
      field.set(user, fieldValue);
    } catch (Exception e) {
      // Handle the problem as appropriate
      e.printStackTrace();
    }
  }
  
  /* Look for Jackson aliases */
  private Map<String, String> getAliases() {
    Map<String, String> fieldAliases = new HashMap<>();

    Field[] fields = User.class.getDeclaredFields();
    for (Field field: fields) {
      Annotation annotation = field.getAnnotation(JsonAlias.class);
      if (annotation != null) {
        String fieldName = field.getName();
        JsonAlias jsonAliasAnnotation = (JsonAlias) annotation;
        String[] aliases = jsonAliasAnnotation.value();
        for (String alias: aliases) {
          fieldAliases.put(alias, fieldName);
        }
      }
    }

    return fieldAliases;
  }
}

With this serializer in place, given a User class similar to:

import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonDeserialize(using = UserDeserializer.class)
public class User {
  private String firstName;
  private String lastName;
  private Integer age;
  private String address;
  @JsonAlias("dateofbirth")
  private LocalDate dateOfBirth;

  // Setters and getters omitted for brevity

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    User user = (User) o;

    if (firstName != null ? !firstName.equals(user.firstName) : user.firstName != null) return false;
    if (lastName != null ? !lastName.equals(user.lastName) : user.lastName != null) return false;
    if (age != null ? !age.equals(user.age) : user.age != null) return false;
    if (address != null ? !address.equals(user.address) : user.address != null) return false;
    return dateOfBirth != null ? dateOfBirth.equals(user.dateOfBirth) : user.dateOfBirth == null;
  }

  @Override
  public int hashCode() {
    int result = firstName != null ? firstName.hashCode() : 0;
    result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
    result = 31 * result + (age != null ? age.hashCode() : 0);
    result = 31 * result + (address != null ? address.hashCode() : 0);
    result = 31 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
    return result;
  }

And the following JSON (I changed to name of the dateofbirth field just for testing aliases):

{"firstName":"John","age":40,"dateofbirth":"1978-03-16"}

You should obtain the appropriate results, consider the following test:

  public static void main(String... args) throws JsonProcessingException {
    User user = new User();
    user.setFirstName("John");
    user.setAge(40);
    user.setDateOfBirth(LocalDate.of(1978, Month.MARCH, 16));

    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

    String json = "{\"firstName\":\"John\",\"age\":40,\"dateofbirth\":\"1978-03-16\"}";

    User reconstructed = mapper.readValue(json, User.class);

    System.out.println(user.equals(reconstructed));
  }

Finally, please, be aware that in order to allow your @KafkaListener to handle null values, you must use the @Payload annotation with required = false, something like:

public class KafkaConsumer {

    @Autowired
    private UserRepository userRepository;

    @KafkaListener(topics = "${spring.kafka.topic.name}")
    public void listen(@Payload(required = false) User user) {
        // Handle null value
        if (user == null) {
          // Consider logging the event
          // logger.debug("Null message received");
          System.out.println("Null message received");
          return;
        }

        // Continue as usual
        User  user = new User(user);
        UserRepository.save(user.getName(), user);
    }
}

See the relevant Spring Kafka documentation and this Github issue and the related commit. This SO question could be relevant as well.

like image 195
jccampanero Avatar answered Oct 04 '22 22:10

jccampanero


The CustomDeserializer is defined for the type String and it is being used to deserialize a User object. That is the reason why the deserializer is working on individual User fields when applied, but not on the entire User object. In order to apply a deserilizer on the entire User object, the CustomDeserializer should be of type User. Something like this:

public class CustomDeserializer extends JsonDeserializer<User> {
@Override
public User deserialize(JsonParser jsonParser, DeserializationContext context) throws
    IOException {
    JsonNode node = jsonParser.readValueAsTree();
    String firstName = null;
    String lastName = null;
    String age = null;
    String address = null;
    if(node.has("firstName") && !node.get("firstName").asText().isEmpty()) {
        firstName = node.get("firstName").asText();
    }
    if(node.has("lastName") && !node.get("lastName").asText().isEmpty()) {
        lastName = node.get("lastName").asText();
    }
    if(node.has("age") && !node.get("age").asText().isEmpty()) {
        age = node.get("age").asText();
    }
    if(node.has("address") && !node.get("address").asText().isEmpty()) {
        address = node.get("address").asText();
    }
    if(firstName == null && lastName == null && age == null && address == null) {
        return null;
    }
    return new User(firstName, lastName, age, address);
}

}

Now, this can be used to deserialize entire User object:

Sample Input:

{
    "firstName" : "",
    "lastName" : "Paul",
    "age" : "31"
}

Will be deserialized into:

User{firstName='null', lastName='Paul', age='31', address='null'}
like image 39
Shyam Baitmangalkar Avatar answered Oct 04 '22 20:10

Shyam Baitmangalkar