JSON Schema 0.0.1 Release

This is the early alpha preview release of JSON Schema support. The goal of this frame work is to provide support for json schema draft v3 and v4.

The project is broken into several core modules.

  • json_core Provides core data types used by all modules.
  • json_parser Parses the schema into kotlin data classes.
  • json_generator Code to generate kotlin code from the parsed schema and a gradle plugin.
  • json_diff A module to diff two json schema objects.

JSON Generator

Given the following simple schema it will generate the following Kotlin code.

This sample is available as an integration test.

{
  "$id": "https://example.com/address.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "An address similar to http://microformats.org/wiki/h-card",
  "type": "object",
  "properties": {
    "post-office-box": {
      "type": "string"
    },
    "extended-address": {
      "type": "string"
    },
    "street-address": {
      "type": "string"
    },
    "locality": {
      "type": "string"
    },
    "region": {
      "type": "string"
    },
    "postal-code": {
      "type": "string"
    },
    "country-name": {
      "type": "string"
    }
  },
  "required": [ "locality", "region", "country-name" ],
  "dependencies": {
    "post-office-box": [ "street-address" ],
    "extended-address": [ "street-address" ]
  }
}

Defining a gradle configuration to generate the Kotlin code, and generating with the sample code.

JsonSchemaGeneratorConfig {
  schema = listOf(
    GeneratorConfig(
      packageBase = "design.animus.kotlin.mp.contract.simple.address",
      outputPath = File("$projectDir/generated"),
      schemaFile = File("$projectDir/main/resources/address.schema.json"),
      schemaName = "Address"
    )
  )
}
./gradlew :tests:simple:jsonSchemaGenerate

We will receive the following Kotlin code.

@Serializable
@ExperimentalSerializationApi
public data class Address(
  @SerialName("post-office-box")
  public val postOfficeBox: String? = null,
  @SerialName("extended-address")
  public val extendedAddress: String? = null,
  @SerialName("street-address")
  public val streetAddress: String? = null,
  public val locality: String,
  public val region: String,
  @SerialName("postal-code")
  public val postalCode: String? = null,
  @SerialName("country-name")
  public val countryName: String
) : IAddress, IJSONSchemaObjectBase {
  public override fun getAJSONSchemaSerializer(): AJSONSchemaSerializer<IJSONSchemaObjectBase> =
      AddressSerializer as AJSONSchemaSerializer<IJSONSchemaObjectBase>
}

@ExperimentalSerializationApi
public object AddressSerializer : AJSONSchemaSerializer<Address>(Address.serializer()) {
  public override val additionalProperties: Boolean = false

  public override val patternProperties: Boolean = false

  public override val patternProperty: Boolean = false

  public override val regexes: Set<Regex> = setOf()

  public override val regexToType: Map<Regex, String> = mapOf()

  public override val properties: List<String> = listOf("post-office-box", "extended-address",
      "street-address", "locality", "region", "postal-code", "country-name")
}

While a simple example it shows the core premises. The generated object inherits off several interfaces, allowing for limiting by various interfaces.

  • IAddress Is a blank interface that extends IJSONSchemaObjectBase
  • IJSONSchemaObjectBase Denotes a simple object.
  • There are additional interfaces if the object has additional or pattern properties.

The next component is the serializer. As noted in the initial demo. The schemas can get a bit complext, and have varying properties. Namely additonalProperties, and patternProperties. This serializer will help pull the keys and values from those more dynamic fields, and serialize them into a flat json object as expected. Conversely deserializing works the same.

JSON Diff

Below is an excerpt from one of the tests of json_diff. A simple Sample class is present with two fields. We compare the objects via diffJsonObjects, which looks for two objects that match the same highest level inheriance. In the above example it would be IAddress. Traversing the json object we find any differences, and denote an action  (i.e. DELETE, UPDATE, CREATE). The function returns a list of changes and the corresponding path. With support for nested keys or arrays.

@ExperimentalSerializationApi
@Serializabledata
class Sample(val message: String,val fruit: String? = null) : IJSONSchemaObjectBase {
  override fun getAJSONSchemaSerializer(): AJSONSchemaSerializer {
    return SampleSerializer as AJSONSchemaSerializer}
  }
}
val old = Sample(message = "Hello")
val new = Sample(message = "Hello", fruit = "banana")
val changes = diffJsonObjects(old, new) ?: error("Changes should not be null")
println(changes)
Sample(message=Hello, fruit=null)2021-03-25 01:54:04,271 [Test worker @coroutine#1] 
DEBUG d.a.k.mp.schemas.json.core.Utils - Attempting to encode value: Sample(message=Hello, fruit=banana) of Sample2021-03-25 01:54:04,274 [Test worker @coroutine#1]
DEBUG d.a.k.mp.schemas.json.core.Utils - Cast to mutable map: {message="Hello", fruit="banana"}{"message":"Hello","fruit":"banana"}2021-03-25 01:54:04,286 [Test worker @coroutine#1]
DEBUG d.a.k.mp.schemas.json.core.Utils - Attempting to encode value: Sample(message=Hello, fruit=null) of Sample{"message":"Hello"}2021-03-25 01:54:04,286 [Test worker @coroutine#1] 
DEBUG d.a.k.mp.schemas.json.core.Utils - Cast to mutable map: {message="Hello"}in diff json objectReceived keys of: [message, fruit]
Changes(action=Delete, path=fruit, old=banana, new=null)

The use case for this library is to quickly visualize how a schema or document has changed between versions.