Skip to content

Commit

Permalink
Fix #2529 (improved null handling for EnumSet)
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Oct 29, 2019
1 parent 9432f40 commit 9d1cd50
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 29 deletions.
1 change: 1 addition & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Project: jackson-databind
(reported by Richard W)
#2520: Sub-optimal exception message when failing to deserialize non-static inner classes
(reported by Mark S)
#2529: Add a tests to ensure `EnumSet` and `EnumMap` work correctly with "null-as-empty"

2.10.0 (26-Sep-2019)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public abstract class ContainerDeserializerBase<T>
*/
protected final NullValueProvider _nullProvider;

/**
* Marker flag set if the <code>_nullProvider</code> indicates that all null
* content values should be skipped (instead of being possibly converted).
*
* @since 2.9
*/
protected final boolean _skipNullValues;

/**
* Specific override for this instance (from proper, or global per-type overrides)
* to indicate whether single value may be taken to mean an unwrapped one-element array
Expand All @@ -39,14 +47,6 @@ public abstract class ContainerDeserializerBase<T>
*/
protected final Boolean _unwrapSingle;

/**
* Marker flag set if the <code>_nullProvider</code> indicates that all null
* content values should be skipped (instead of being possibly converted).
*
* @since 2.9
*/
protected final boolean _skipNullValues;

protected ContainerDeserializerBase(JavaType selfType,
NullValueProvider nuller, Boolean unwrapSingle) {
super(selfType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.NullValueProvider;
import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.util.AccessPattern;
import com.fasterxml.jackson.databind.util.ClassUtil;
Expand All @@ -30,6 +32,21 @@ public class EnumSetDeserializer

protected JsonDeserializer<Enum<?>> _enumDeserializer;

/**
* Handler we need for dealing with nulls.
*
* @since 2.10.1
*/
protected final NullValueProvider _nullProvider;

/**
* Marker flag set if the <code>_nullProvider</code> indicates that all null
* content values should be skipped (instead of being possibly converted).
*
* @since 2.10.1
*/
protected final boolean _skipNullValues;

/**
* Specific override for this instance (from proper, or global per-type overrides)
* to indicate whether single value may be taken to mean an unwrapped one-element array
Expand Down Expand Up @@ -57,35 +74,64 @@ public EnumSetDeserializer(JavaType enumType, JsonDeserializer<?> deser)
}
_enumDeserializer = (JsonDeserializer<Enum<?>>) deser;
_unwrapSingle = null;
_nullProvider = null;
_skipNullValues = false;
}

/**
* @since 2.7
* @deprecated Since 2.10.1
*/
@SuppressWarnings("unchecked" )
@Deprecated
protected EnumSetDeserializer(EnumSetDeserializer base,
JsonDeserializer<?> deser, Boolean unwrapSingle) {
this(base, deser, base._nullProvider, unwrapSingle);
}

/**
* @since 2.10.1
*/
@SuppressWarnings("unchecked" )
protected EnumSetDeserializer(EnumSetDeserializer base,
JsonDeserializer<?> deser, NullValueProvider nuller, Boolean unwrapSingle) {
super(base);
_enumType = base._enumType;
_enumClass = base._enumClass;
_enumDeserializer = (JsonDeserializer<Enum<?>>) deser;
_nullProvider = nuller;
_skipNullValues = NullsConstantProvider.isSkipper(nuller);
_unwrapSingle = unwrapSingle;
}

public EnumSetDeserializer withDeserializer(JsonDeserializer<?> deser) {
if (_enumDeserializer == deser) {
return this;
}
return new EnumSetDeserializer(this, deser, _unwrapSingle);
return new EnumSetDeserializer(this, deser, _nullProvider, _unwrapSingle);
}

@Deprecated // since 2.10.1
public EnumSetDeserializer withResolved(JsonDeserializer<?> deser, Boolean unwrapSingle) {
if ((_unwrapSingle == unwrapSingle) && (_enumDeserializer == deser)) {
return withResolved(deser, _nullProvider, unwrapSingle);
}

/**
* @since 2.10.1
*/
public EnumSetDeserializer withResolved(JsonDeserializer<?> deser, NullValueProvider nuller,
Boolean unwrapSingle) {
if ((_unwrapSingle == unwrapSingle) && (_enumDeserializer == deser) && (_nullProvider == deser)) {
return this;
}
return new EnumSetDeserializer(this, deser, unwrapSingle);
return new EnumSetDeserializer(this, deser, nuller, unwrapSingle);
}

/*
/**********************************************************
/* Basic metadata
/**********************************************************
*/

/**
* Because of costs associated with constructing Enum resolvers,
* let's cache instances by default.
Expand All @@ -104,31 +150,37 @@ public Boolean supportsUpdate(DeserializationConfig config) {
return Boolean.TRUE;
}

@Override // since 2.10.1
public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException {
return constructSet();
}

@Override // since 2.10.1
public AccessPattern getEmptyAccessPattern() {
return AccessPattern.DYNAMIC;
}

/*
/**********************************************************
/* Contextualization
/**********************************************************
*/

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
{
Boolean unwrapSingle = findFormatFeature(ctxt, property, EnumSet.class,
final Boolean unwrapSingle = findFormatFeature(ctxt, property, EnumSet.class,
JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
JsonDeserializer<?> deser = _enumDeserializer;
if (deser == null) {
deser = ctxt.findContextualValueDeserializer(_enumType, property);
} else { // if directly assigned, probably not yet contextual, so:
deser = ctxt.handleSecondaryContextualization(deser, property, _enumType);
}
return withResolved(deser, unwrapSingle);
}

@Override // since 2.10.1
public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException {
return constructSet();
return withResolved(deser, findContentNullProvider(ctxt, property, deser), unwrapSingle);
}

@Override // since 2.10.1
public AccessPattern getEmptyAccessPattern() {
return AccessPattern.DYNAMIC;
}

/*
/**********************************************************
/* JsonDeserializer API
Expand Down Expand Up @@ -168,13 +220,15 @@ protected final EnumSet<?> _deserialize(JsonParser p, DeserializationContext ctx
// What to do with nulls? Fail or ignore? Fail, for now (note: would fail if we
// passed it to EnumDeserializer, too, but in general nulls should never be passed
// to non-container deserializers)
Enum<?> value;
if (t == JsonToken.VALUE_NULL) {
return (EnumSet<?>) ctxt.handleUnexpectedToken(_enumClass, p);
if (_skipNullValues) {
continue;
}
value = (Enum<?>) _nullProvider.getNullValue(ctxt);
} else {
value = _enumDeserializer.deserialize(p, ctxt);
}
Enum<?> value = _enumDeserializer.deserialize(p, ctxt);
/* 24-Mar-2012, tatu: As per [JACKSON-810], may actually get nulls;
* but EnumSets don't allow nulls so need to skip.
*/
if (value != null) {
result.add(value);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.fasterxml.jackson.databind.deser.filter;

import java.util.EnumMap;
import java.util.EnumSet;

import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.ObjectMapper;

public class NullConversionsForEnumsTest extends BaseMapTest
{
static class NullValueAsEmpty<T> {
@JsonSetter(nulls=Nulls.AS_EMPTY)
public T value;
}

static class NullContentAsEmpty<T> {
@JsonSetter(contentNulls=Nulls.AS_EMPTY)
public T values;
}

static class NullContentSkip<T> {
@JsonSetter(contentNulls=Nulls.SKIP)
public T values;
}

/*
/**********************************************************
/* Test methods, for container values as empty
/**********************************************************
*/

private final ObjectMapper MAPPER = newJsonMapper();

public void testEnumSetAsEmpty() throws Exception
{
NullValueAsEmpty<EnumSet<ABC>> result = MAPPER.readValue(aposToQuotes("{'value': null }"),
new TypeReference<NullValueAsEmpty<EnumSet<ABC>>>() { });
assertNotNull(result.value);
assertEquals(0, result.value.size());
}

public void testEnumMapAsEmpty() throws Exception
{
NullValueAsEmpty<EnumMap<ABC, String>> result = MAPPER.readValue(aposToQuotes("{'value': null }"),
new TypeReference<NullValueAsEmpty<EnumMap<ABC, String>>>() { });
assertNotNull(result.value);
assertEquals(0, result.value.size());
}

/*
/**********************************************************
/* Test methods, for container contents, null as empty
/**********************************************************
*/

// // NOTE: no "empty" value for Enums, so can't use with EnumSet, only EnumMap

public void testEnumMapNullsAsEmpty() throws Exception
{
NullContentAsEmpty<EnumMap<ABC, String>> result = MAPPER.readValue(aposToQuotes("{'values': {'B':null} }"),
new TypeReference<NullContentAsEmpty<EnumMap<ABC, String>>>() { });
assertNotNull(result.values);
assertEquals(1, result.values.size());
assertEquals("", result.values.get(ABC.B));
}

/*
/**********************************************************
/* Test methods, for container contents, skip nulls
/**********************************************************
*/


public void testEnumSetSkipNulls() throws Exception
{
NullContentSkip<EnumSet<ABC>> result = MAPPER.readValue(aposToQuotes("{'values': [ null ]}"),
new TypeReference<NullContentSkip<EnumSet<ABC>>>() { });
assertNotNull(result.values);
assertEquals(0, result.values.size());
}

public void testEnumMapSkipNulls() throws Exception
{
NullContentSkip<EnumMap<ABC, String>> result = MAPPER.readValue(aposToQuotes("{'values': {'B':null} }"),
new TypeReference<NullContentSkip<EnumMap<ABC, String>>>() { });
assertNotNull(result.values);
assertEquals(0, result.values.size());
}
}

0 comments on commit 9d1cd50

Please sign in to comment.