Menu Close

Dynamically Parsing JSON to Arbitrary Java Classes using the Jackson JSON Library

In our advanced predictive analytics platform, we’ve built some very innovative techniques that let users model aggregated data using arbitrary grouping of target entities and weighted measures synthesized from the underlying source data. For example, a user can define a weighted measure called “financial stability” that is a weighted combination various financial measures of a country (such as GDP, debt ratio, exports, etc.) Furthermore, the user can then apply this measure across a composite entity such as the G20 countries and build a model for a question such as “what happens when the financial stability of the G20 drops by 20%?”

The system is able to translate this back to the underlying measures spread across all of the countries that make up the G20 and then build the predictive model to answer the question.  The results can then also be aggregated back up to the country groupings so we can show the user the effect on the other measures for the G20 as a whole. This makes it significantly easier for an analyst to build sweeping models without having to individually adjust a daunting number of underlying measure values.

In order to support this capability, we’ve built a sophisticated query engine that sits on top of ElasticSearch. The query engine accepts a high-level query that may be for composite entities and synthetic measures and breaks it down into very complex ElasticSearch queries for all of the individual value which aggregating and filtering them as needed to support the high-level query’s requirements.  Here is a relatively simple example of such a high-level query:

{
   "scenarioByCountry":{
      "scenarioId":"5b27023b-af38-4213-83de-31fcf7115c19",
      "measureName":"Financial Activity",
      "valueField":"Value",
      "aggregation":"Average",
      "effectiveDateTag":"201911",
      "scenarioDataStart":"2019-09-01",
      "sortOrder":"Ascending",
      "filters":[
         {
            "date":{
               "fieldName":"Date",
               "from":"2019-01-01",
               "to":"2025-12-30"
            }
         },
         {
            "matchString":{
               "fieldName":"Country ",
               "values":[
                  "Russia",
                  "Estonia",
                  "Romania"
               ]
            }
         }
      ]
   }
}

 

This query returns the average value of the synthesized measure “Financial Stability” sorted from lowest to highest for the three specified countries over the given date period.

Since we have what is effectively a dynamic query language, we did not want to have to develop a full-scale parser for it. Instead, we decided to develop a set of query classes and use the Jackson JSON library to parse an incoming query string into the appropriate query class based on the type of query.

Usually, to do this in Jackson you would use its type annotations to map from a base class into the appropriate subclass based on a property in the JSON. For our query classes, this would look something like this:

@JsonTypeInfo(//
		use = JsonTypeInfo.Id.NAME, //
		include = JsonTypeInfo.As.EXISTING_PROPERTY, //
		property = "queryType", //
		visible = true)
@JsonSubTypes({ //
		@Type(value = ScenarioByCountryQuery.class, name = "scenarioByCountry"), //
		@Type(value = ScenarioByMeasureQuery.class, name = "scenarioByMeasure")
 })

 

Unfortunately, this technique means you have to include a property in the JSON (queryType in this case) which makes it not as obvious what kind of query is being run:

{
   "queryType":"scenarioByCountry",
   "scenarioId":"5b27023b-af38-4213-83de-31fcf7115c19",
   "measureName":"Financial Activity",
   "valueField":"Value",
   "aggregation":"Average",
   "effectiveDateTag":"201911",
   "scenarioDataStart":"2019-09-01",
   "sortOrder":"Ascending",
   "filters":[
      {
         "date":{
            "fieldName":"Date",
            "from":"2019-01-01",
            "to":"2025-12-30"
         }
      },
      {
         "matchString":{
            "fieldName":"Country ",
            "values":[
               "Russia",
               "Estonia",
               "Romania"
            ]
         }
      }
   ]
}

 

This was not very intuitive and did not feel natural for a query language, particularly when multiple queries were being submitted in a batch. We really wanted the query type to be the top element (as in the first JSON example). To do this, we needed to develop a custom deserializer that could map from a top level JSON property to the appropriate query subclass. This turned out to be quite simple and we were even able to leverage the existing Jackson annotations for configuring in from the base class.

public class TypedDeserializer<R> extends StdDeserializer<R> {

	/**
	 * Version id for serialization.
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * Map of the query name to the Java type that it should be deserialized to.
	 */
	private Map<String, Class<?>> nameToTypeMap;

	// ---------------------------------------------------------------------
	// Constructors.
	// ---------------------------------------------------------------------

	public TypedDeserializer(Class<R> targetClass) {
		super(targetClass);

		// Use the JsonSubType annotations on the main query interface to determine all of the various
		// types that it could map to.
		JsonSubTypes.Type[] queryTypes = targetClass.getAnnotation(JsonSubTypes.class).value();

		// Now store these in a mapping from the type name to the Java class for its query.
		nameToTypeMap = new LinkedHashMap<String, Class<?>>();
		for (JsonSubTypes.Type queryType : queryTypes) {
			nameToTypeMap.put(queryType.name(), queryType.value());
		}
	}

	// ---------------------------------------------------------------------
	// JsonDeserializer methods.
	// ---------------------------------------------------------------------

	@Override
	@SuppressWarnings("unchecked")
	public R deserialize(JsonParser parser, DeserializationContext context) throws IOException {

		ObjectMapper objectMapper = (ObjectMapper) parser.getCodec();

		ObjectNode object = objectMapper.readTree(parser);

		for (Map.Entry<String, Class<?>> mapEntry : nameToTypeMap.entrySet()) {
			String propertyName = mapEntry.getKey();
			Class<?> targetClass = mapEntry.getValue();

			if (object.has(propertyName)) {
				JsonNode targetConfigNode = object.get(propertyName);
				return (R) objectMapper.treeToValue(targetConfigNode, targetClass);
			}
		}
			
		String name = object.fieldNames().next();
		throw new IllegalArgumentException("Unrecognized type '" + name + "'.");
	}
}

 

Our custom deserializer can be used in a Jackson ObjectMapper by configuring it in a module:

objectMapper = new ObjectMapper();
		
SimpleModule module = new SimpleModule();
module.addDeserializer(Query.class, new TypedDeserializer(Query.class));
objectMapper.registerModule(module);

 

When it initializes, the custom typed deserializer will use the @JsonSubTypes annotation on the Query class to determine all of the possible query subclasses that an incoming JSON query request can be parsed into.

@JsonSubTypes({
		@JsonSubTypes.Type(value = BaselineAggregationByCountryQuery.class, name="baselineByCountry"),
		@JsonSubTypes.Type(value = BaselineAggregationByDateQuery.class, name="baselineByDate"),
		@JsonSubTypes.Type(value = BaselineAggregationByMeasureQuery.class, name="baselineByMeasure"),
		@JsonSubTypes.Type(value = ScenarioAggregationByCountryQuery.class, name="scenarioByCountry"),
		@JsonSubTypes.Type(value = ScenarioAggregationByDateQuery.class, name="scenarioByDate"),
		@JsonSubTypes.Type(value = ScenarioAggregationByMeasureQuery.class, name="scenarioByMeasure")
})
public interface Query<R> {

 

Finally, actually parsing an incoming query string is very straightforward:

public Query<?> parseQuery(String jsonQuery) {
		
		Query<?> parsedQuery = null;
		try {
			parsedQuery = objectMapper.readValue(jsonQuery, Query.class);
		} catch( Exception e ) {
			throw new MiddlewareException("Failed to parse JSON query: " + e.getMessage(), e);
		}
		
		return parsedQuery;
	}

 

With thoughtful composition of the Query class hierarchy to support common query elements such as filters and result sort orders, this parsing technique provides an easy way to support a very sophisticated query syntax while leveraging the Jackson JSON library to handle all the heavy lifting of parsing the incoming queries. In our Middleware, we accept the query as the body of a POST request which gives us the string to pass into the parser. This makes for a very clean API that easily extensible; to add a new query type, we just implement a new subclass and register it in the @JsonSubypes annotation of the base query class.

Posted in Blog

Leave a Reply

Your email address will not be published. Required fields are marked *