Marcus Wyche

Jackson Enum Serialization

Ever have an Enum that you needed to deserialize different than it's value in json. I recently needed to do just that and since I use Spring Boot with Jackson a lot I went to find an example of how to do this online and found the examples lacking. So I decided to write this blog hopefully it helps someone else out. In this example the customer is sending you a version:

{
  "version": "1.0.0"
}

But that is not a valid variable name in Java, so you'll either need to keep it as a string or represent it via enum which allows your code to be fully aware of exactly which versions are being read and you'll immediately know when your customer introduces a new version. Below you can see how I've defined the Java Enum representing versions:

@Log
@ToString
public enum Version {
    VERSION_1("1.0.0"),

    VERSION_1_0_2("1.0.2");

    static Map<String, Version> VERSION_MAP;

    static {
        VERSION_MAP = Stream.of(Version.values())
                .collect(toMap(Version::getVersion, Function.identity()));
    }
    String version;

    Version(String version){
        this.version = version;
    }

    public String getVersion(){
        return version;
    }

    public static Version findVersionFromVersionString(String versionString){
        Version version = VERSION_MAP.get(versionString);
        log.info("Versions: " + VERSION_MAP);

        if(version!=null)
            return version;

        throw new NoSuchElementException(versionString + " is not a proper version.");
    }
}

In order to have Jackson natively do the conversion you'll have to write a custom deserializer. Below you can see a custom deserializer for the version enum:

/**
 * Deserializer for Version
 */
@Log
public class JacksonVersionDeserializer extends StdDeserializer<Version> {
    public JacksonVersionDeserializer(){
        this(null);
    }

    protected JacksonVersionDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Version deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        log.fine("Text " + node.asText());
        return Version.findVersionFromVersionString(node.asText());
    }
}

Testing

We can't say this works without a test. Below I have a parameterized test that ensures I can pass in all the version values via raw json and they can be converted to the enum.

@ParameterizedTest
@EnumSource(Version.class)
public void testWorks(Version xVersion) throws JsonProcessingException {
    Version version = mapper.readValue("\"" + xVersion.version +"\"", Version.class);
    log.info("Version: " + xVersion);
    assertEquals(xVersion, version);
}

Bloopers

Where did I totally screw up trying to create the Deserializer and get this to work.

  1. In the deserializer it's important to use node.asText() and not node.toString() because unless you've changed your toString() method it will likely print something like, version=1.0.0 which is not what you want to deserialize.

  2. Testing this I ran into two main issues:

    1. Expected space separating root-level values this was happening when I forgot to surround the String I was passing down to Jackson with quotes. You can see a test for this blooper below:
    @Test
        public void testBlooperDeserializaerJsonParseException() {
            // Tried several different iterations of this
            JsonParseException exception = assertThrows(JsonParseException.class, () ->  mapper.readValue(Version.VERSION_1_0_2.version, Version.class));
            log.info(""+exception);
        }
    
    1. non-static inner classes like this can only by instantiated using default, no-argument constructor I ran into this issue after getting frustrated with issue 1 and trying to just wrap the whole this in a POJO. You can see a test showing this issue below:
    @Test
        public void testBlooperNeedCustomSerializerForThisToWork() throws JsonProcessingException {
            Example example = new Example();
            example.setVersions(Arrays.asList(Version.VERSION_1));
    
            InvalidDefinitionException exception = assertThrows(InvalidDefinitionException.class, () -> mapper.readValue(mapper.writeValueAsString(example), Example.class));
            log.info("" + exception);
        }
    

    My true problem here is that I likely needed a custom serializer to properly write out my enum. Proving that thought is for another entry though.

Whew weee the formatting above is terrible I need to get prism working ASAP. We'll hopefully I'll look back at this one day to see my growth. If you've made it this far, thanks for taking the time out to read this!