Skip to content

Proposal for allOf, anyOf and oneOf

jrohit54 edited this page May 18, 2021 · 8 revisions

Overview

This is a proposal for the implementation of oneOf, anyOf and allOf. It is broken into a set of rules for rewriting schemas containing allOf and anyOf types into simpler schemas and a method for deserializing oneOf rules into the correct type. These rules are specified in a high level way and a set of problematic schemas are included to provide details.

NOTE: not is not considered in this proposal. Suggestions on how to handle that keyword are welcome.

Schema Rewriting

Since allOf and anyOf deal in the composition of types, they could be handled by rewriting the types containing them, so that they no longer appear in the schema. Once this is done, the existing rules for type generation could be employed. The rules here are specified loosely, since some schema combinations could lead to problems that are combinatoric in nature. For instance, an allOf containing several oneOfs could result in a cross product operation happening. See the Problematic Schemas section for examples.

allOf Rules

  • description: the union of the description properties, joined with and.
  • javaInterfaces: the intersection of the input javaInterface array.
  • extends: the intersection of the input extends properties. This should consider the most specific common superclasses.
  • properties: the union of the properties.
  • required:
    • for booleans, the logical or of the 'required' properties.
    • for arrays, the union of the 'required' arrays.
  • type: the common type or null if the types differ.

anyOf Rules

  • description: the union of the description properties, joined with or
  • javaInterfaces: the union of the input javaInterface arrays.
  • extends: the intersection of the input extends properties. This should consider superclasses.
  • properties: the union of the properties.
  • required:
    • for booleans, the logical and of the 'required' properties.
    • for arrays, the intersection of the 'required' arrays.
  • type: the common type or the schema becomes a oneOf with values for each type.

Problematic Schemas

Cross Products Along the Breadth

Schemas with oneOf nested in allOf could produce a large number of types, if the implementation attempts to produce a type for every possible valid combination.

{
  "allOf": [
    {
      "oneOf": [
        { "ref": "a.json" },
        { "ref": "b.json" }
      ],
      "oneOf": [
        { "ref": "c.json" },
        { "ref": "d.json" }
      ]
    }
  ]
}

To avoid this, oneOfs should be treated with these rules when found in allOf:

  • the result of operations on oneOf nested in an allOf will result in one oneOf being created, with one nested schema for each type found in all the sub schemas.
  • common 'type' schemas are merged using the rules of anyOf
  • if the oneOf contains only one schema, it is collapsed

These rules result in at most one type being created (for { "type": "object" }). All other types involved will come from the existing set of types (Integer, String, etc.)

Cross Products Along the Depth

Schemas where anyOf has nested schemas with co-prime cycle lengths could produce a large number of types, if the implementation attempts to produce every unique hierarchy level.

{
  "type": "object",
  "properties": {
    "child": {
      "anyOf": [
        { "ref": "#/definitions/a" },
        { "ref": "#/definitions/b" }
      ]
    }
  },
  "definitions": {
    "a": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/definitions/c" },
        "a": { "type": "string" }
      }
    },
    "b": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/definitions/d" },
        "b": { "type": "string" }
      }
    },
    "c": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/definitions/a" },
        "c": { "type": "string" }
      }
    },
    "d": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/definitions/e" },
        "d": { "type": "string" }
      }
    },
    "e": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/definitions/b" },
        "e": { "type": "string" }
      }
    }
  }
}

Currently, I do not know the best way to resolve this elegantly. Fully collapsing types involved in cycles may be the best option. That would result in this being the rewritten form of this schema.

{
  "type": "object",
  "properties": {
    "child": {
      "type": "object",
      "properties": {
        "child": { "ref": "#/properties/child" },
        "a": { "type": "string" },
        "b": { "type": "string" },
        "c": { "type": "string" },
        "d": { "type": "string" },
        "e": { "type": "string" }
      }
    }
  }
}

Deserializing Types

Custom deserialization will be required to handle oneOf, since properties can only specify one type. To accomplish this, the needed deserialization code will be statically generated in the generated types.

Supporting Jackson1 and Jackson2

Using a combination of @ JsonProperty and a nested static JsonDeserializer, oneOf could easily be implemented for Jackson. The generated code would follow this general pattern.

  JCodeModel codeModel = new JCodeModel();
        try {
            URL source= new URL("file:src/main/resources/schema/DummyApi.json");
            GenerationConfig config = new DefaultGenerationConfig() {
                @Override
                public boolean isGenerateBuilders() {
                    return true;
                }
                public SourceType getSourceType(){
                    return SourceType.JSON;
                }
            };
            SchemaMapper mapper =new SchemaMapper(new RuleFactory(config, new GsonAnnotator(config), new SchemaStore()), new SchemaGenerator());
            mapper.generate(codeModel, "Accession", "com.pojogenerated", source);
            File dir = new File("src/main/java");
            if(dir.exists()){
                System.out.println("dir available");
                codeModel.build(dir);
            }else{
                System.out.println("dir not available");
            }

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

Supporting GSON

GSON 2.3 introduced a @ JsonAdapter annotation. This could be used to bind a property to a nested static TypeAdapter specifically for that type, in that context. There is currently not an example for this.