Search code examples
javajsonjackson2

Serializing/deserializing aliased primitives in Jackson without too much boilerplate


I have a class I want to deserlialize into a Java into a class. One of the class attributes is itself a serializable class which has one primitive (String) parameter and really represents an aliased String, i.e., a String with some extra stuff around it to make sure it comes in the correct format. This is useful in a variety of applications (e.g., identifiers), in this case it's a Cron string that has to have specific representation. So I have

class ScheduleConfigurations {
    private Cron cron;
    private Set<Cron> overrides;
    // other attributes, getters/setters, constructor
}


class Cron {
    private String cronString;

    private void validate() { // validates string has correct representation }
    // getters, setters, constructor
}

Obviously the standard Jackson deserializer expects a JSON of the format

{
    "cron": {
        "cronString": "someCronString",
    "overrides": [
        { "cronString": "someCronString1" },
        { "cronString": "someCronString2" }
    ]
}

However, the Cron class is clearly just an aliased String. I'd want the above to be serialized/deserialized as

{
    "cron": "someCronString",
    "overrides": ["someCronString1", "someCronString2"]
}

I can make that happen with a custom serializer with a bunch of boilerplate, but I am looking for an elegant way to say to Jackson "hey, this class has one String attribute, just pass through it and treat its serialized representation as its only attribute".


Solution

  • Since you want the entire object to be (de)serialized as a string, it's very easy:

    @JsonSerialize(using = ToStringSerializer.class)
    class Cron {
        private String cronString;
        public Cron(String cronString) { // Called by Jackson when deserializing from JSON string
            this.cronString = cronString;
        }
        @Override
        public String toString() { // Called by Jackson when serializing to JSON
            return this.cronString;
        }
    }
    

    Since the class is just a wrapper around a string, you don't even need getter, setter, or other constructor, not even for other Java code, though you can of course still have them.

    Test

    ScheduleConfigurations configObj = new ScheduleConfigurations();
    configObj.setCron(new Cron("someCronString"));
    configObj.setOverrides(new LinkedHashSet<>(Arrays.asList(
            new Cron("someCronString1"), new Cron("someCronString2"))));
    ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
    System.out.println(mapper.writeValueAsString(configObj));
    
    String json = "{\r\n" + 
                  "  \"cron\" : \"someCronString\",\r\n" + 
                  "  \"overrides\" : [ \"someCronString1\", \"someCronString2\" ]\r\n" + 
                  "}";
    ScheduleConfigurations configObj = new ObjectMapper().readValue(json, ScheduleConfigurations.class);
    System.out.println("cron = " + configObj.getCron());
    System.out.println("overrides = " + configObj.getOverrides());
    

    Output

    {
      "cron" : "someCronString",
      "overrides" : [ "someCronString1", "someCronString2" ]
    }
    
    cron = someCronString
    overrides = [someCronString1, someCronString2]