JSON schema wrapper classes

Overview

The JsonObject class is a dynamic data structure—a Map of values. However, in cases where a JSON schema is defined, you might want to leverage the schema to provide statically typed getter and setter methods.

InsuranceSuite combines the benefits of a dynamic data structure with the benefits of static typing. To effect this combination, the software can generate statically typed classes that wrap a JsonObject class in a layer of statically-typed getters and setters. Once a wrapper has been layered on a JsonObject, you can access the object as if it were a DTO object into which data is deserialized.

Such statically typed wrapper classes always extend the base JsonWrapper class. They also have typed getters and setters for all properties declared in the schema. In addition, the wrapper classes have helper methods for parsing in a schema-aware way. The REST API framework is integrated such that you can use the JsonWrapper subclasses anywhere that you can use a JsonObject. The framework then automatically wraps or unwraps the JsonObject object as needed.

Package and class naming

PolicyCenter generates the schema wrapper classes in Java under the jsonschema namespace. PolicyCenter generates the classes in a package named jsonschema.<schema package>.<name>.v<schema version>.

For example, the schema gw.pc.activities.activities-1.0 would have wrapper classes generated under jsonschema.gw.pc.activities.activities.v1_0. Each definition in the schema then has a generated class with a name that matches the definition name. Package, class, and getter/setter names that would otherwise be illegal Java identifiers are adjusted as necessary to make them legal.

Wrapping and unwrapping

A new wrapper class can be instantiated with an empty JsonObject by calling the no argument constructor on the wrapper class. A wrapper class can be applied to an existing JsonObject by calling the static wrap method on the class.

The static wrap method always preserves null properties. Moreover, calling wrap(null) returns null back. For example:
var json : JsonObject
var activityDetail = ActivityDetail.wrap(json)
Turning the wrapper back into a JsonObject is as simple as calling the unwrap method on a JsonWrapper:
function toJson(activity : Activity) : JsonObject {
  var activityDetail = new ActivityDetail() // Creates a new, empty JsonObject and wraps it with an ActivityDetail()
  activityDetail.subject = activity.Subject
  return activityDetail.unwrap()
}
Parse helpers

JsonWrapper classes have a pair of static parse methods that can be used to parse JSON strings. The methods use the same schema definition that was used to generate the classes. These can be helpful in situations like tests. For example, you can invoke a REST API first. Then, you can parse the response directly into a wrapper. Lastly, you can easily use this wrapper in test assertions.

Parsing this way violates the late-binding principle mentioned in JsonObject class. However, this manner of parsing can be helpful when debugging test code. This parsing method is also ideal for developing customer-specific code in that the developer does not have to consider extending the underlying schema.

Additional properties

If a JSON schema definition specifies additionalProperties, the generated wrapper class has getAdditionalProperty and setAdditionalProperty methods. These methods essentially provide a statically-typed wrapper on top of a basic Map get or put.

Lists and subobjects

Subobjects retrieved with the statically-typed helpers are themselves wrapped on retrieval. These wrappers are not stable. Rather, they are created for each individual call. Likewise, when statically-typed helpers are invoked to set a subobject, they implicitly unwrap the wrapper and set the underlying JsonObject as the value of the property. In general, the lack of stability of the JsonWrapper references is not a problem as the underlying JsonObject contains the data and is always the same.

However, lists of subobjects present a somewhat more awkward case. While retrieving a List of subobjects, the underlying List is wrapped in a special list that dynamically wraps each object as it is retrieved. The list also unwraps JsonWrapper objects as they are added to the List object. When setting a List of subobjects, a new List has to be created and set on the underlying JsonObject. The values of the original list are unwrapped and replaced. Thus, reading the values of a list is straightforward and looks analogous to the equivalent operation with a normal DTO-style class. However, while setting a List value, modifications to the original List object are not reflected in the underlying JsonObject. For example, the following code works as expected:
var contact : ContactDetail
var addresses = new List<AddressDetail>()
addresses.add(new AddressDetail() { :addressLine1 = "100 Main St." })
addresses.add(new AddressDetail() { :addressLine1 = "200 Main St." })
contact.addresses = addresses
But this code fails because calling contact.addresses = addresses implicitly copies the List object. The second call to add is not reflected in the ContactDetail JsonObject:
var contact : ContactDetail
var addresses = new List<AddressDetail>()
addresses.add(new AddressDetail() { :addressLine1 = "100 Main St." })
contact.addresses = addresses
addresses.add(new AddressDetail() { :addressLine1 = "200 Main St." })
There are three ways to avoid this problem:
  • Make sure to add all elements to a newly constructed list prior to calling the setter on the wrapper object.
  • Re-retrieve the list from the wrapper object for subsequent additions rather than using the original list.
  • Use the addToX helper method that is generated for each list.
The addToX helper methods have an additional helpful quality in that they create the underlying List object if the property is null. This code can produce a NullPointerException:
var contact : ContactDetail
contact.addresses.add(new AddressDetail() { :addressLine1 = "100 Main St." })
While this code avoids that problem:
var contact : ContactDetail
contact.addToAddresses(new AddressDetail() { :addressLine1 = "100 Main St." })

Lists of scalar values do not present the same difficulty and do not require wrapping. However, they still benefit from the addToX helper method to auto-create the backing List.

Generating the classes

Unlike our existing code generators, the JSON schema wrapper classes must be generated explicitly. Then, the classes must be checked in to source code rather than being automatically generated as part of the build. This check-in requirement is partly a pragmatic concession. The concession is due to the level of effort required in order to build a bullet-proof code generator that is properly incremental and fast enough to run as part of the build. The check-in requirement is also partly a deliberate design decision. The decision gives Guidewire more freedom to make the wrapper classes optional or to change the code generator over time without impacting existing uses.

In order to generate the classes, add the fully-qualified name of the schema to generate classes to config/integration/schemas/codegen-schemas.txt.

The code generator can be invoked either from an IntelliJ run command or from the command line:
  • In IntelliJ, you can add a run command for com.guidewire.tools.json.JsonSchemaWrapperCodegenTool.
  • From the command-line, you can run the jsonSchemaCodegen task.

One unfortunate pitfall is that you can sometimes get into a state in which running the code generator can leave code in a non-compiling state. In this case, you need to get back to a compiling state before you can run the code generator to fix the problem. Using source control to revert the generated classes to their previous state is generally the best option to return to a compiling state.