skip to Main Content

I want to write a custom serializer that, when it encounters a null value for a set, serializes it as an empty set. I want to pair that with a deserializer which deserializes an empty set back to null. If the collection has elements, it can be serialized/deserialized as normal.

I’ve written a couple of deserializers and they work well but the methods I used there don’t seem applicable to collections. For example, I wrote this to turn empty strings into nulls:

JsonNode node = p.readValueAsTree();        
        String text = (Objects.isNull(node) ? null : node.asText());
        if (StringUtils.isEmpty(text)) {
            return null;
        }

I don’t think this will work because JsonNode doesn’t have an asSet() method.

I’ve found examples online that look promising but it seems like all the examples of working with collections involve working with the elements inside the collection, not the collection itself.

So far, I’ve been hand-coding this process but I’m sure there’s a better way to deal with it.

I’m at the point of figuring it out by trial and error so any examples, ideas, or advice would be appreciated.

Here’s what I’m thinking it should look like:

@JsonComponent
public class SetDeserializer extends Std???Deserializer<Set<?>> {
    
    public SetDeserializer() {
        super(Set.class);
    }

    @Override
    public Set<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = p.readValueAsTree();        
        Set<?> mySet = (Objects.isNull(node) ? null : node.asSet());
        if (CollectionUtils.isEmpty(mySet)) {
            return null;
        }
        return super().deserialize(p, ctxt);
    }

}

2

Answers


  1. This is quite simple using ObjectMapper.

    public static List<String> deserializeStringArray(JsonNode node) throws IOException
    {
      ObjectMapper mapper = new ObjectMapper();
      boolean isArrayNode = Objects.nonNull(node) && node.isArray();
    
      if (isArrayNode && !node.isEmpty()) {
        ObjectReader reader = mapper.readerFor(mapper.getTypeFactory().constructCollectionType(List.class, String.class));
        return reader.readValue(node);
      }
      else if (isArrayNode && node.isEmpty()) {
        return Collections.emptyList();
      }
      else {
        return null;
      }
    }
    

    This returns a List of the Nodes elements by first verifying that the node is an array and is not empty. But, if the list is an empty array node, we return an empty list, and if it isn’t an arrayNode, then we return null.

    Based on your requirements, I wasn’t sure if the contents of your array list were empty (ie null) or the json node itself is expected to be null. If the JsonNode itself is expected to be null, then you can easily modify this to return an empty list when it is null:

    public static List<String> deserializeStringArray(JsonNode node) throws IOException
    {
      ObjectMapper mapper = new ObjectMapper();
      if (Objects.nonNull(node) && node.isArray()) {
        ObjectReader reader = mapper.readerFor(mapper.getTypeFactory().constructCollectionType(List.class, String.class));
        return reader.readValue(node);
      }
      else {
        return Collections.emptyList();
      }
    }
    

    You can test this via the following

    JsonNode arrayNode = mapper.createArrayNode().add("Bob").add("Sam");
    System.out.println(deserializeStringArray(arrayNode));
    
    JsonNode emptyArrayNode = mapper.createArrayNode();
    System.out.println(deserializeStringArray(emptyArrayNode));
    

    Here’s how to use the above code, to deserialize an object into an array of animals

    @Component
    public class AnimalDeserializer extends JsonDeserializer<Animal>
    {
      ObjectMapper mapper;
      @Override
      public Animal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException
      {
        mapper = (ObjectMapper) p.getCodec();
        JsonNode node = p.getCodec().readTree(p);
    
        JsonNode arrayNode = node.get("myArrayField");
    
        List<Animal> animals = deserializeAnimalArray(arrayNode);
    
        return animals;
      }
    
      public List<Animal> deserializeAnimalArray(JsonNode node) throws IOException
      {
        boolean isArrayNode = Objects.nonNull(node) && node.isArray();
    
        if (isArrayNode && !node.isEmpty()) {
          ObjectReader reader = mapper.readerFor(mapper.getTypeFactory().constructCollectionType(List.class, String.class));
          return reader.readValue(node);
        }
        else if (isArrayNode && node.isEmpty()) {
          return Collections.emptyList();
        }
        else {
          return null;
        }
      }
    }
    

    You can reverse this to get your JsonNode.

    Edit: Added a working deserializer example

    Login or Signup to reply.
  2. To make it work as it is required:

    • Serialise null Set as an empty JSON Array []
    • Deserialise an empty JSON Array [] as null
    • Configure it global

    We need to use at the same time:

    • com.fasterxml.jackson.databind.JsonSerializer to generate an empty JSON Array []
    • com.fasterxml.jackson.databind.util.StdConverter to convert an empty Set or List to null
    • com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector to register serialiser and converters for all properties.

    Below example shows all above components and how to use them:

    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializationFeature;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.introspect.Annotated;
    import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
    import com.fasterxml.jackson.databind.json.JsonMapper;
    import com.fasterxml.jackson.databind.util.StdConverter;
    import lombok.Data;
    import org.springframework.util.CollectionUtils;
    
    import java.io.IOException;
    import java.util.Collection;
    import java.util.List;
    import java.util.Set;
    
    public class SetApp {
        public static void main(String[] args) throws JsonProcessingException {
            var mapper = JsonMapper.builder()
                    .enable(SerializationFeature.INDENT_OUTPUT)
                    .annotationIntrospector(new EmptyAsNullCollectionJacksonAnnotationIntrospector())
                    .build();
    
            var json = mapper.writeValueAsString(new CollectionsPojo());
            System.out.println(json);
            var collectionsPojo = mapper.readValue(json, CollectionsPojo.class);
            System.out.println(collectionsPojo);
        }
    }
    
    class EmptyAsNullCollectionJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
    
        @Override
        public Object findNullSerializer(Annotated a) {
            if (Collection.class.isAssignableFrom(a.getRawType())) {
                return NullAsEmptyCollectionJsonSerializer.INSTANCE;
            }
            return super.findNullSerializer(a);
        }
    
        @Override
        public Object findDeserializationConverter(Annotated a) {
            if (List.class.isAssignableFrom(a.getRawType())) {
                return EmptyListAsNullConverter.INSTANCE;
            }
            if (Set.class.isAssignableFrom(a.getRawType())) {
                return EmptySetAsNullConverter.INSTANCE;
            }
            return super.findDeserializationConverter(a);
        }
    }
    
    class NullAsEmptyCollectionJsonSerializer extends JsonSerializer<Object> {
    
        public static final NullAsEmptyCollectionJsonSerializer INSTANCE = new NullAsEmptyCollectionJsonSerializer();
    
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeStartArray();
            gen.writeEndArray();
        }
    }
    
    class EmptySetAsNullConverter extends StdConverter<Set<?>, Set<?>> {
    
        public static final EmptySetAsNullConverter INSTANCE = new EmptySetAsNullConverter();
    
        @Override
        public Set<?> convert(Set<?> value) {
            if (CollectionUtils.isEmpty(value)) {
                return null;
            }
            return value;
        }
    }
    
    class EmptyListAsNullConverter extends StdConverter<List<?>, List<?>> {
    
        public static final EmptyListAsNullConverter INSTANCE = new EmptyListAsNullConverter();
    
        @Override
        public List<?> convert(List<?> value) {
            if (CollectionUtils.isEmpty(value)) {
                return null;
            }
            return value;
        }
    }
    
    @Data
    class CollectionsPojo {
        private List<Integer> nullList;
        private List<Integer> emptyList = List.of();
        private List<Integer> listOfOne = List.of(1);
        private Set<String> nullSet;
        private Set<String> emptySet = Set.of();
        private Set<String> setOfOne = Set.of("One");
    }
    

    Above code prints:

    {
      "nullList" : [ ],
      "emptyList" : [ ],
      "listOfOne" : [ 1 ],
      "nullSet" : [ ],
      "emptySet" : [ ],
      "setOfOne" : [ "One" ]
    }
    
    CollectionsPojo(nullList=null, emptyList=null, listOfOne=[1], nullSet=null, emptySet=null, setOfOne=[One])
    

    You can also register converters and null serialiser using annotations directly on the field you want:

    @JsonSerialize(nullsUsing = NullAsEmptyCollectionJsonSerializer.class)
    @JsonDeserialize(converter = EmptyListAsNullConverter.class)
    private List<Integer> nullList;
    
    @JsonSerialize(nullsUsing = NullAsEmptyCollectionJsonSerializer.class)
    @JsonDeserialize(converter = EmptySetAsNullConverter.class)
    private Set<String> nullSet;
    

    In this case you do not need to register EmptyAsNullCollectionJacksonAnnotationIntrospector.

    See also:

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search