Reputation: 41
I am trying to make jackson deserialize missing properties that represent collections into empty collections instead of NULL. I've tried a few different things and this is the latest that isn't working.
If there is a way to make any potential solution globally configured for all POJO deserialization that would be great.
Given the following json, which is missing the property 'assets':
{
"name":"my-layer",
"code": "ly1",
"types":["type1", "type2"],
"private": false
}
Given the following POJO:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class CreateLayerRequest {
@NotBlank
@JsonProperty(required = true)
private String name;
@NotBlank
@JsonProperty(required = true)
private String code;
@Default
@NotNull
@JsonProperty(value = "private", required = true)
private Boolean privateLayer = Boolean.FALSE;
@Default
@JsonSetter(nulls = Nulls.AS_EMPTY)
@JsonProperty("types")
private Set<UUID> types = new HashSet<>();
@Default
@JsonSetter(nulls = Nulls.AS_EMPTY)
@JsonProperty("assets")
private Set<UUID> assets = new HashSet<>();
}
Upvotes: 4
Views: 6197
Reputation: 41
configured my fix to be global
@Configuration
public class MyAppConfiguration implements WebMvcConfigurer {
/**
* allows POJOs using json filtering to serialize all properties when not being filtered
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) converter)
.getObjectMapper()
// not related to this thread, this is for property filtering when serializing POJOs
.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false))
// these modules are related to to this thread
.registerModule(
new SimpleModule().addDeserializer(Set.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(List.class, new CollectionDeserializer())
)
.registerModule(
new SimpleModule().addDeserializer(Map.class, new CustomMapDeserializer())
);
}
}
}
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
private static class CollectionDeserializer<T>
extends JsonDeserializer<Collection<T>>
implements ContextualDeserializer {
private JavaType type;
private final ObjectMapper mapper = new ObjectMapper();
@Override
public JsonDeserializer<?> createContextual(
DeserializationContext deserializationContext,
BeanProperty beanProperty) {
final JavaType javaType = deserializationContext.getContextualType() != null
? deserializationContext.getContextualType()
: beanProperty.getMember().getType();
return new CollectionDeserializer<>(javaType);
}
@Override
public Collection<T> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
try {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new HashSet<>());
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return deserializeJsonToCollection(jsonParser, new ArrayList<>());
}
} catch (IllegalArgumentException e) {
log.warn("unable to deserialize array property in request");
throw e;
}
final String message = "unable to deserialize collection";
log.warn(message);
throw new CollectionDeserializationException(message);
}
private Collection<T> deserializeJsonToCollection(
JsonParser jsonParser,
Collection<T> collection) throws IOException {
((ArrayNode) jsonParser
.getCodec()
.readTree(jsonParser))
.forEach(item -> {
try {
final T value = mapper.readValue(
String.format("\"%s\"", item.textValue()),
type.getContentType()
);
collection.add(value);
} catch (IOException e) {
final String message = String.format(
"unable to deserialize value [%s] to type [%]",
item.textValue(),
type.getContentType()
);
log.warn(message);
throw new CollectionDeserializationException(message);
}
});
return collection;
}
@Override
public Collection<T> getNullValue(DeserializationContext ctxt) {
if (Set.class.isAssignableFrom(type.getRawClass())) {
return new HashSet<>();
}
if (List.class.isAssignableFrom(type.getRawClass())) {
return new ArrayList<>();
}
return null;
}
}
@Slf4j
private static class CustomMapDeserializer<T, K> extends JsonDeserializer<Map<T, K>> {
private final ObjectMapper mapper = new ObjectMapper();
private final TypeReference<Map<T, K>> mapTypeReference = new TypeReference<>() {
};
@Override
public Map<T, K> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) {
try {
return mapper.readValue(jsonParser, mapTypeReference);
} catch (Exception e) {
final String message = "unable to deserialize map";
log.warn(message);
throw new MapDeserializationException(message, e);
}
}
@Override
public Map<T, K> getNullValue(DeserializationContext ctxt) {
return new HashMap<>();
}
}
Upvotes: 0