Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of oneOf support. #1193

Open
piotrtomiak opened this issue Feb 5, 2021 · 7 comments
Open

Implementation of oneOf support. #1193

piotrtomiak opened this issue Feb 5, 2021 · 7 comments

Comments

@piotrtomiak
Copy link

I've tried to avoid oneOf for a long time due to issues with Pojo generation, but with my recent large and complex JSON schema I just couldn't do it. So I have implemented support for oneOf, which would map to POJOs where possible.

I would love to contribute that support to the project, but I want to know whether the idea sounds good for you and the contribution would be not be declined immediately.

There are 5 main scenarios I have handled. Except for scenario 1, properties on the schema with oneOf are ignored.

  1. There is single branch in oneOf. In this case the branch is treated as extends.

  2. All of branches are objects and there is a common constant string field present. E.g.:

"framework-context": {
  "type": "object",
  "oneOf": [ {
      "properties": {
        "kind": { "type": "string","const": "node-package"},
        (...)
      }
    }, {
      "properties": {
        "kind": {"type": "string","const": "script-urls"},
        (...)
      }
  }]
}

It results in generation of a super class with enum "kind" property and following annotations:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(name = "node-package", value = FrameworkContextNodePackage.class),
    @JsonSubTypes.Type(name = "script-urls", value = FrameworkContextScriptUrls.class)
})
  1. All of the branches are objects and objects are distinguishable based on the required fields.
"source": {
  "oneOf": [
    {
      "type": "object",
      "properties": { "file": {...}, "offset": {...} },
      "required": [ "file", "offset" ]
    },
    {
      "type": "object",
      "properties": { "module": {...}, "symbol": {...} },
      "required": [ "symbol" ]
    }
  ]
}

It results in generation of an empty super class with following annotations (deduction is available since Jackson 2.12.0):

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
    @JsonSubTypes.Type(SourceFileOffset.class),
    @JsonSubTypes.Type(SourceSymbol.class)
})
  1. There are two branches, of which one is array with items of the same type as the other branch. This is an optional convenience feature.
"generic-contributions": {
  "oneOf": [
    {
      "type": "array",
      "items": {"$ref": "#/definitions/generic-contribution"}
    },
    {"$ref": "#/definitions/generic-contribution"}
  ]
}

This results in a collection class extending ArrayList or HashSet with custom deserializer:

@JsonDeserialize(using = GenericContributions.MyDeserializer.class)
public class GenericContributions extends ArrayList<GenericContribution> {
  
  public static class MyDeserializer extends JsonDeserializer<GenericContributions> {
    
    @Override
    public GenericContributions deserialize(JsonParser parser, DeserializationContext deserializationContext)
      throws IOException {
      GenericContributions result = new GenericContributions();
      JsonToken token = parser.currentToken();
      if (token == JsonToken.START_ARRAY) {
        while (parser.nextToken() != JsonToken.END_ARRAY) {
          result.add(parser.readValueAs(GenericContribution.class));
        }
      }
      else {
        result.add(parser.readValueAs(GenericContribution.class));
      }
      return result;
    }
  }
}
  1. In any other case, when each branch is of different type, or is object and object branches are distinguishable - all object branches are processed as per pt. 1 or pt. 2 and an umbrella class with single field value and custom deserializer is created. E.g:
"type": {
  "oneOf": [
    { "type": "string" },
    { "$ref": "#/definitions/complex-type" },
    { "$ref": "#/definitions/type-reference" }
  ]
}

Results in (getter/setter removed from the example):

@JsonDeserialize(using = Type.MyDeserializer.class)
public class Type {

  /**
   * Type: {@code String | TypeBase}
   */
  public Object value;

  public static class MyDeserializer  extends JsonDeserializer<Type> {


    @Override
    public Type deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException {
      Type result = new Type();
      JsonToken token = parser.currentToken();
      if (token == JsonToken.VALUE_STRING) {
        result.value = parser.readValueAs(String.class);
      }
      else if (token == JsonToken.START_OBJECT) {
        result.value = parser.readValueAs(TypeBase.class);
      }
      else {
        deserializationContext.handleUnexpectedToken(Object.class, parser);
      }
      return result;
    }
  }
}

Where TypeBase is created as per pt. 2.

@ThanksForAllTheFish
Copy link

any news on this? I ended up splitting my json schema into multiple files so that all pojos are generated, though in a sort of detached state, and then combined those pojos in the generic map obtained from the main schema, but it is not very nice.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "oneOf": [
    {
      "$ref": "definitions.json#/definitions/def1"
    },
    {
      "$ref": "definitions.json#/definitions/def2"
    }
  ]
}

having this schema generates a java class like

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({

})
@Generated("jsonschema2pojo")
public class AssigneeEvent {

    @JsonIgnore
    private Map<String, Object> additionalProperties = new HashMap<String, Object>();

    @JsonAnyGetter
    public Map<String, Object> getAdditionalProperties() {
        return this.additionalProperties;
    }

    @JsonAnySetter
    public void setAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
    }
}

and having individual pojos generated from definitions.json I only need to use strings for the top level fields and set the entire pojo as additional property

var def1 // pojo from definitions.json#/definitions/def1
var event = new AssigneeEvent()
event.setAdditionalProperty("someProp", def1.getSomeProp())
event.setAdditionalProperty("someObj", def1.getSomeObj())

so it is not too bad, but obviously event.setSomeProp(...) would be better and more type-safe

@piotrtomiak
Copy link
Author

piotrtomiak commented Mar 18, 2021

@ThanksForAllTheFish I am able to work on the contribution, but I need some info from the maintainer (cc @joelittlejohn), before I dedicate time.

@tillias
Copy link

tillias commented Mar 30, 2021

This idea sounds crazy good and this is exactly the thing we're missing badly. Please let me know if you need some help with it.

Our use-case is to use following stuff as input

{
  "$id": "https://example.com/some.schema.json",
  "title": "some",
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "someEnum": {
      "type": "object",
      "oneOf": [
        {
          "properties": {
            "businessId": {
              "const": "001"
            },
            "name": {
              "const": "first"
            }
          },
          "required": ["businessId", "name"]
        },
        {
          "properties": {
            "businessId": {
              "const": "002"
            },
            "name": {
              "const": "second"
            }
          },
          "required": ["fachId", "name"]
        }
      ]
    }
  },

  "required": ["id", "someEnum"]
}

And generate enums or value objects from it. At the moment generator just generates empty file for someEnum

@Murik
Copy link

Murik commented Jul 19, 2021

any news?

1 similar comment
@pantinor
Copy link

pantinor commented Mar 8, 2022

any news?

@pantinor
Copy link

pantinor commented Mar 8, 2022

@piotrtomiak @joelittlejohn Is there any way to do the oneOf pojo generation with a workaround in the json, without factoring out the oneOf array from separate common properties of the objects?

@rosti-il
Copy link

Hi @piotrtomiak

What do you think about the following idea of implementation of the oneOf support?

Make a little bit extended POJO with setters that, in case of oneOf, not only set the value of the appropriate field but also remove (set null) values of all other non-common fields of all other branches. This approach doesn't need any special serializer/deserializer and/or class inheritance. For the regular serializers/deserializers like ObjectMapper in the Jackson project such extended POJO should look exactly like regular POJO.

For example, for this part of schema:

"identification": {
  "type": "object",
  "oneOf": [ {
      "properties": {
        "idNumber": { "type": "string" }
      }
    }, {
      "properties": {
        "person": { "$ref": "person.json" },
      }
  }]
}

The generated Identification POJO will look like this one:

public class Identification {

    private String idNumber;
    private Person person;

    public String getIdNumber() {
        return idNumber;
    }

    public void setIdNumber(String idNumber) {
        this.idNumber = idNumber;
        this.person = null;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
        this.idNumber = null;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants