Search code examples
javajsonenumsjacksondeserialization

Read JSON array as enums with type based on property


My JSON input is this:

{
  // other fields
  "context": [
    {
      "id": "age",
      "name": "Age",
      "type": "string",
      "value": "730 days +"
    },
    {
      "id": "v2Score",
      "name": "V2 Score",
      "type": "number",
      "value": 5.9
    },
    {
      "id": "maturity",
      "name": "Maturity",
      "type": "string",
      "value": "High"
    },
    {
      "id": "coverage",
      "name": "Product Coverage",
      "type": "string",
      "value": "Low"
    },
    {
      "id": "threat_intensity",
      "name": "Threat Intensity",
      "type": "string",
      "value": "Very Low"
    },
    {
      "id": "threat_recency",
      "name": "Threat Recency",
      "type": "string",
      "value": "No recorded events"
    },
    {
      "id": "threat_sources",
      "name": "Threat Sources",
      "type": "string",
      "value": "No recorded events"
    }
  ]
}

and my Context POJO class looks like this

class Context {
  private final Age age;
  private final Maturity maturity;
  private final Coverage coverage;
  private final ThreatIntensity threatIntensity;
  private final ThreatRecency threatRecency;
  
  // constructor, getters
}

All of the above fields in Context are enums. Using Age as an example:

enum Age {
  @JsonProperty("0 - 7 days")
  LESS_THAN_ONE_WEEK,
  @JsonProperty("7 - 30 days")
  ONE_WEEK_TO_ONE_MONTH,
  @JsonProperty("30 - 365 days")
  ONE_MONTH_TO_ONE_YEAR, 
  @JsonProperty("365 - 730 days")
  ONE_TO_TWO_YEARS, 
  @JsonProperty("730 days + ")
  MORE_THAN_TWO_YEARS
}

the object in the array with "id": "age" corresponds to the Age enum type, and "value": "730 days +" corresponds to Age.MORE_THAN_TWO_YEARS.

How can I deserialize this array of objects into individual enums? Each object in the array corresponds to exactly one type of enum (i.e., there will never be two objects in the array with the same id), but the objects in the array are not guaranteed to be in the same order every time. Is there a way to do this with annotations, such as @JsonTypeInfo? Additionally, while I don't expect any of the enums to be missing, I'd like to be able to pass "missing" enums as null. For example, if the object with "id": "maturity" was not present in the array, the associated Maturity enum object should be deserialized as null. So far data returned from this API has either included all or none of these elements (resulting in "context": []), although I have no guarantee that's true everywhere.

I can also ignore any objects in the array with an id that I don't care about (in this case, v2Score and threat_sources). I included them in the example to demonstrate that there may be data outside of what I want that should be ignored. Additionally, the "name" and "type" fields in each of the objects can be ignored, as only the id is needed to determine the enum type, and only "value" is needed to select which enum of that type.

I got about as far as this before getting stuck on what to do. I'm doing my best to avoid making switch statements over the id or value fields, as additions in the future would likely cause bugs.

@JsonCreator
Context(
  @JsonProperty("age") Age age,
  @JsonProperty("Maturity") Maturity maturity,
  @JsonProperty("coverage") Coverage coverage,
  @JsonProperty("threat_intensity") ThreatIntensity intensity,
  @JsonProperty("threat_recency") ThreatRecency recency,
) {
  // basic constructor
}

For the above input, the following object should be created:

context.getAge() // Age.MORE_THAN_TWO_YEARS
context.getMaturity() // Maturity.HIGH
context.getCoverage() // Coverage.LOW
context.getThreatIntensity() // ThreatIntensity.VERY_LOW
context.getThreatRecency() // ThreatRecency.NONE

Not all of the elements in the input array are (logically) enums, but all of the elements that I care about are enums.

I'm guessing I need to use @JsonTypeInfo on each of the constructor arguments in some way? From what I know of how it works, if I was able to annotate Enum like this, it would deserialize the individual enums correctly. I'm not sure how to translate the array into individual parameters though.

@JsonTypeInfo(
  use = JsonTypeInfo.Id.CUSTOM,
  as = JsonTypeInfo.As.EXTERNAL_PROPERTY,
  property = "id",
  defaultImpl = Void.class,
  visible = false)
@JsonSubTypes({
  @Type(value=Age.class, name="age"),
  @Type(value=Maturity.class, name="maturity")
  @Type(value=Coverage.class, name="coverage")
  @Type(value=ThreatIntensity.class, name="threat_intensity")
  @Type(value=ThreatRecency.class, name="threat_recency")})
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

  // annotating the implicitly-declared valueOf(String) method for Enums
  @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
  @JsonIgnoreProperties(ignoreUnknown = true)
  public static abstract <T extends Enum<T>> T valueOf(@JsonProperty("value") String name);
}

I do also know how to make a custom deserializer for this, but doing so makes the code much more brittle, especially if it's extended or changed in the future.


Solution

  • I've confirmed that what I am attempting is not possible, as java does not provide the means to deserialize to "any generic enum" https://github.com/FasterXML/jackson-databind/issues/2739