Home > Enterprise >  Custom serializer/deserializer for a collection, specifically a set
Custom serializer/deserializer for a collection, specifically a set

Time:01-18

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);
    }

}

CodePudding user response:

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

CodePudding user response:

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])

See also:

  • Related