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

Generating a POJO using oneOf, anyOf or allOf with constraints #392

Open
CasperDB opened this issue Jul 21, 2015 · 41 comments
Open

Generating a POJO using oneOf, anyOf or allOf with constraints #392

CasperDB opened this issue Jul 21, 2015 · 41 comments

Comments

@CasperDB
Copy link

I'm generating a POJO using the jsonschema2pojo-maven-plugin and need to pass constraints in using oneOf, anyOf & allOf.

When I try the following code in a schema validator it works:

{
  "type": "object",
  "properties": {
    "name": {
      "description": "The advertised product",
      "type": "string"
    },
    "id" : {
      "oneOf": [
        { "type": "string", "maxLength": 5 },
        { "type": "number", "minimum": 0 }
      ]
    }
  }
}

However, the POJO after building has no constraints:

 @JsonProperty("id")
    private Object id;

I would like to see some restrictions annotated, is this not supported with the plugin yet?

@joelittlejohn
Copy link
Owner

No, I'm afraid these are not supported.

It would be useful if you could explain how you would expect this to work in this case? What Java would you like to see produced?

@joelittlejohn
Copy link
Owner

(see also #91)

@CasperDB
Copy link
Author

I'm not sure what I want it to look like yet... My intention is to replace a jaxb object with a jackson object. Something similar to the xsd choice @xmlelement:

<xsd:element name="name" type="xsd:string" minOccurs="0"/>
            <xsd:choice>
                <xsd:element name="address" type="address"/>
                <xsd:element name="phone-number" type="phoneNumber"/>
                <xsd:element name="note" type="xsd:string"/>
            </xsd:choice>
private String name;

    @XmlElements(value = { 
            @XmlElement(name="address", 
                        type=Address.class),
            @XmlElement(name="phone-number", 
                        type=PhoneNumber.class),
            @XmlElement(name="note", 
                        type=String.class)
    })

I'm currently looking at Jackson annotations that might be useful.

@samskiter
Copy link
Collaborator

Maybe you could blow oneOf out into several properties.

 @JsonProperty("idString")
 @Nullable
    private String idString;

 @JsonProperty("idNumber")
 @Nullable
    private String idNumber;

  public void setIdString(String newIdString)
  {
    idNumber = null;
    idString = newIdString
  }

...

It would require a custom gson/jackson serialization/deserialization step for that part though. It could be pretty generic and we could write it once into the jsonschema2pojo library.

@amilinko
Copy link

Maybe oneOf rule can be implemented as class inheritance. In the example above, it could look like this:

public abstract class id {
}

public class child1 extends id {
    public String type;
}

public class child2 extends id {
    public int type;
}

I think allOf and anyOf do not make sense in java.

@samskiter
Copy link
Collaborator

True, but we have to work with gson/Jackson right? How will they know how
to deserialise the json and pick the right subclass?

On Mon, 10 Aug 2015 17:47 amilinko notifications@github.com wrote:

Maybe oneOf rule can be implemented as class inheritance. In the
example above, it could look like this:

public abstract class id {
}
public class child1 extends id {
public String type;
}
public class child2 extends id {
public int type;
}

I think allOf and anyOf do not make sense in java.


Reply to this email directly or view it on GitHub
#392 (comment)
.

@amilinko
Copy link

Yes, it would require Jackson (not sure about gson). There is an example of serialization/deserialization here. It is described in examples 4, 5 and 6.

@samskiter
Copy link
Collaborator

Looks like you'd end up doing reflective stuff like:

if (anInstance.isKindOfClass(child1.class) {
...

There's still a big question mark over how you decide which one to decode it as. Do you run up a JSON schema validator and see which one it passes? You probably need some extension to json schema to mark which one it is as some kind of enum.

oneOf is really nice from a validation side but really awkward from a model side. If you look at swagger, they've ditched oneOf support because of this. here's an alternate schema definition that achieves the same thing I think:

{
  "type": "object",
  "properties": {
    "name": {
      "description": "The advertised product",
      "type": "string"
    },
    "id" : {
      "properties": {
         "stringId" : { "type": "string", "maxLength": 5 },
         "numberId" :  { "type": "number", "minimum": 0 }
      },
      "maxProperties" : 1,
      "minProperties" : 1
    }
  }
}

@ctrimble
Copy link
Collaborator

I was considering submitting a PR for oneOf, anyOf, and allOf that just generates the fields required to properly bind the data, so that at least those constructs are usable. Solid support for these semantics will probably require providing a runtime dependency with (de)serializers for Jackson and objects to model these types.

If a runtime dependency is something the project could support, then code that works like this (for the id example) could be generated:

OneOf id = pojo.getId();

// test the type
pojo.getId().is(Double.class)

// cast
Double number = pojo.getId().as(Double.class)

// with java 8 and functional interfaces
pojo.getId().isType(Double.class).then(id->...)

@rpatrick00
Copy link

oneOf does make sense in Java if you think about polymorphic types. For example:

                    "dataSourceBindings": {
                        "type": "array",
                        "items": {
                            "oneOf": [
                                {
                                    "genericDataSourceBinding": {
                                        "type": "object",
                                        "properties": {
                                            "dataSourceName": {
                                                "type": "string"
                                            }
                                        },
                                        "required": [ "dataSourceName" ]
                                    }
                                },
                                {
                                    "multiDataSourceBinding": {
                                        "type": "object",
                                        "properties": {
                                            "dataSourceName": {
                                                "type": "string"
                                            },
                                            "algorithmType": {
                                                "type": "string",
                                                "enum": [ "Failover", "Load-Balancing"]
                                            }
                                        },
                                        "required": [ "dataSourceName" ]
                                    }
                                }
                            ]
                        }
                    }

What I would have hoped to see generated was a DataSourceBinding class, and two subclasses of it: GenericDataSourceBinding and MultiDataSourceBinding.

@ctrimble
Copy link
Collaborator

@rpatrick00 That is an interesting idea. With $ref in the mix, the types can form a cyclic graph and not just tree structures, so linking oneOf to single inheritance will be problematic.

@rpatrick00
Copy link

Maybe so but the current behavior is a problematic too.

The object containing the dataSourceBinding property has this:

List dataSourceBindings

and nothing else inside this is generated...not DataSourceBinding, GenericDataSourceBinding, or MultiDataSourceBinding. I can understand the fact that you don;t want to deal with polymorphism but it should, at minimum, generate the classes for types defined inside the oneOf...

@ctrimble
Copy link
Collaborator

I have created some basic test cases and an empty rule in my fork feature_unions. Is there any interest in working on this as a group?

@rpatrick00
Copy link

I would be happy to contribute to oneOf, provided I understand the various use cases that you want to support. For me, the most important use case is for polymorphic object/array types.

@ctrimble
Copy link
Collaborator

@rpatrick00 is the example you have provided consistent with draft 4 of the specification? It seems like you have added an extra level under oneOf.

It seems like it will be hard to alter the types being composed in a oneOf, anyOf, or allOf. Perhaps we could go the other direction and use Java Intersection Types when defining the method signature. For example:

public interface Named {
  public String getName();
}
public interface Described {
  public String getDescription();
}

a.json

{
  "type": "object",
  "javaInterfaces": ["Named", "Described"],
  "properties": {
    "name": { "type": "string" },
    "description": { "type": "string" },
    "a": { "type": "string" }
  }
}

b.json

{
  "type": "object",
  "javaInterfaces": ["Named", "Described"],
  "properties": {
    "name": { "type": "string" },
    "description": { "type": "string" },
    "b": { "type": "string" }
  }
}

c.json

{
  "type" "object",
  "properties" : {
    "children": {
      "type": "array",
      "items": {
        "oneOf": [
           { "$ref": "a.json" },
           { "$ref": "b.json" }
        ]
      }
    }
  }
}

C.java

public class C {
  public <T implements Named & implements Described> List<T> getChildren() { ... }
  public <T implements Named & implements Described> void setChildren( List<T> children ) { ... }
}

This would probably require providing some special Jackson (de)serializers.

@rpatrick00
Copy link

I am not sure Java Intersection Types accomplish the same thing as Polymorphism. Your c.json shows what I need but in my case, the only "shared interface" is the superclass and the subclasses share no other common "interface" or properties (if they did, they would be in the super type, wouldn't they).

Other parts of my code are accomplishing this starting with Java and JAXB/Jackson bindings. In this module, I need to start with JSON and was hoping to generate the Java code. I was able to make something close to what I wanted work (the generated code uses List<Object> instead of List<DataSourceBinding> but it works for me).

@ctrimble
Copy link
Collaborator

We could also constrain on the common superclass, so that wouldn't be a problem. I do think that the common superclass would need to come from the way the children are specified and not from their participation in a oneOf. Otherwise, we would end up in a situation where types participating in multiple oneOf statements would need multiple superclasses.

I have started a test project locally, to see what can be done for Jackson support. It seems like we would need some annotations like the following to properly support oneOf:

  @OneOf(
    @TypeOption(type=A.class),
    @TypeOption(type=B.class)
  )
  public <T> T value;

I will push that up to github when I have some minimal examples working.

NOTE: Due to Java Annotations limitations on cycles, etc., custom annotation bindings is probably not the way to go. Instead, purpose built deserializers is probably the best way to make Jackson handle these Json Schema constructs.

@ctrimble
Copy link
Collaborator

It looks like static inner JsonDeserializers may be the best way to implement these features. I have created a gist showing an example of what we could create for oneOf. I didn't polish this, but it works.

The basic pattern would be:

public class GENERATED_TYPE {
  public static FIELD$JsonDeserializer extends JsonDeserializer<FIELD_TYPE> {
    @Override public FIELD_TYPE deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectCodec codec = jp.getCodec();
        TreeNode tree = jp.readValueAsTree();

        // pick type here, continue parse with codec.
  }
  @JsonDeserialize(using=FIELD$JsonDeserializer.class)
  private FIELD_TYPE FIELD;
  ...
}

@joelittlejohn really interested in what you think of this.

@joelittlejohn
Copy link
Owner

I'm not sure that this works as a general approach, how do you pick the type in the general case? Were you suggesting these deserializers are generated by the tool or are written manually to choose the type based on whatever manual checks are required?

@ctrimble
Copy link
Collaborator

I see this as two problems, computing the correct merged schema and getting Jackson to deserialize what was computed. For the deserialization part, I am proposing generating the deserializers in the model. Determining a reasonable and correct way to represent the types seems like a challenge and shouldn't be attempted unless we can work with the types generated.

@ctrimble
Copy link
Collaborator

As far as computing the types, a wiki page should probably be started. High level, the needed rules seem something like:

For allOf:

  1. A new type for allOf is always generated.
  2. Property lists are merged. If properties overlap, then they must be of the same type or one must directly extend the other (for $ref).
  3. The required set of properties would widen, to include any property that is required in any schema.

For anyOf:

  1. A new type for anyOf is always generated.
  2. Property lists are merged, If properties overlap, then the rules for oneOf are applied.
  3. The required set of properties would narrow, to only include properties that are required in all schemas.

For oneOf:

  1. New types are only generated for children that are inlined.
  2. The property's type would be the most specific common superclass of the types involved.
  3. Tests for being that type are created (perhaps implemented as a static method on the type with signature boolean isThisType(JsonTree tree)).
  4. The tests are applied in order, inside a generated JsonDeserializer that is applied to the property containing the oneOf.

This strategy would only work for Jackson. I have no clue how GSON deals with types like this. I guess Map, List, or the target (de)serializers generic JSON representation could be returned for other tools.

@ctrimble
Copy link
Collaborator

It looks like something similar can be accomplished with GSON 2.3+ using @JsonAdapter and TypeAdapter.

@ctrimble
Copy link
Collaborator

I have added a page to the wiki with a proposal for implementing these keywords.

@ctrimble ctrimble mentioned this issue Jan 14, 2016
@cheeray
Copy link

cheeray commented Jun 14, 2016

I had folked and created a simple solution for "oneOf", see oneOfTypesProduceObjects Test. No inheritance,just enum and fields mapping. Basic concept is finding a best match based on required and optional fields, because sometimes there may not have any common fields in "oneOf".

@ben-manes
Copy link
Collaborator

It would be nice if oneOf was supported, e.g. using JsonSubTypes. Even if only supported by Jackson it would be a win.

@ctrimble
Copy link
Collaborator

ctrimble commented Sep 1, 2016

@ben-manes if the functionality was constrained to a single discriminator field, it probably would not be hard to support both Jackson and Gson. For anything more complex, you could just bail out to a generic type like JsonNode for Jackson or JsonElement for Gson.

After attempting more complete support for oneOf/anyOf/allOf several times, I do not think it is feasible. There is just too much impedance between what Java object models can represent and what JSON Schema can specify. The project might as well settle for very limited support.

@ben-manes
Copy link
Collaborator

Trying to force Java OO onto JsonSchema definitely doesn't work. The codegen works beautifully for RPCs, but for more complex cases I bail out to JsonPath. All I'd hope for is more support for the easy cases rather than trying to bridge paradigms completely.

@s13o
Copy link
Contributor

s13o commented Mar 17, 2017

Hi all
Guys, I need this feature and ready to participate.
I have some questions

@ctrimble
Copy link
Collaborator

@s13o I put that proposal out there and the consensus seems to be that these keywords are just too problematic to generate good solutions for. If you want basic support, I would create a PR where these keywords generate fields of Object. That could be achieved with minimal work and should result in the mappers injecting their generic node types.

@s13o
Copy link
Contributor

s13o commented Mar 17, 2017

Proposal is detailed enough. It more than 1 year old already. Let's stop talking and start to do something. In a feature-branch.

@JLLeitschuh
Copy link
Contributor

JLLeitschuh commented Jun 1, 2017

What about a getter that returns a railroad oriented data type?
https://fsharpforfunandprofit.com/rop/

For example,

"oneOf": [
           { "type": "number" },
           { "type": "string" }
        ]

could generate a getter that returns something like Functional Java's Either data type.

In practice you'd probably end up generating a class for each field that can be multiple types.
Something more like:

interface SomethingEither {
    default Optional<Number> number() { return Optional.empty() }
    default Optional<String> string() { return Optional.empty(); }
}

So you would get code generated with a method something like this:

SomethingEither getSomethingEither() {
    if(something instanceOf Number) {
        return new SomethingEither() { public Optional<Number> number() { return Optional.of((Number) something); } };
    }
    if(something instanceOf String) {
        return return new SomethingEither() { public Optional<String> string() { return Optional.of((String) something); } };
    }
}

@joelittlejohn
Copy link
Owner

@JLLeitschuh I like this. I can imagine SomethingEither having another method marked @JsonCreator used by Jackson when creating these values from JSON input.

One question remains though - and it's the same challenge with many of the other solutions - how does the @JsonCreator method make sense of incoming JSON when applied to arbitrarily complex JSON objects?

This schema is easy to accommodate:

"oneOf": [{ "type": "number" },
          { "type": "string" }]

this one not so much:

"oneOf": [{ "type": "object", "properties" : ...},
          { "type": "object", "properties" : ...}]

Lets take a really simple, concrete example:

"oneOf": [{ "type": "object", "properties" : {"foo": {"type: "string"}}},
          { "type": "object", "properties" : {"foo": {"type: "number"}}}]

How do we know which of the Either values should be populated for an incoming piece of JSON? What's the general rule?

@JLLeitschuh
Copy link
Contributor

JLLeitschuh commented Jun 2, 2017

You'd have to generate some sort of unnamed type that you'd have to define just like you'd have to do for a field that defines only one object. Jackson would end up serializing into one of these objects and then you'd have to do the same casting thing.

More concretely:
{ "type": "object", "properties" : {"foo": {"type: "string"}}}
would become something like class SomethingAnnonA and
{ "type": "object", "properties" : {"foo": {"type: "number"}}}
would become something like class SomethingAnnonB.

The method would be:

SomethingEither getSomethingEither() {
    if(something instanceOf SomethingAnnonA) {
        return new SomethingEither() { public Optional<SomethingAnnonA> somethingAnnonA() { return Optional.of((SomethingAnnonA) something); } };
    }
    if(something instanceOf SomethingAnnonB) {
        return return new SomethingEither() { public Optional<SomethingAnnonB> somethingAnnonB() { return Optional.of((SomethingAnnonB) something); } };
    }
}

I don't really know how jackson works under the hood, I've never worked with jackson's API directly. I've only ever used it through spring where it does most things automagically.

@joelittlejohn
Copy link
Owner

This is the part that's missing:

Jackson would end up serializing into one of these objects

It's not possible for Jackson to choose one of these types. Once you have a value, returning it correctly via your Either type would be possible, but how do you correctly choose which type the value should have in the example I gave?

@JLLeitschuh
Copy link
Contributor

Try/catch jackson deserializing every possible type that something could be?
What would the type of something be by default in the current implementation? A map? A string?

@JLLeitschuh
Copy link
Contributor

I think you can use @JacksonInject ObjectMapper into @JsonCreator methods with correct configuration. Again, not something I've played with.

@hadim
Copy link

hadim commented Jul 30, 2017

Hello,

I am jumping in this conversation to see what is the current status. Do you guys think it's actually possible to generate this Java code with feature such as anyOf?

@JLLeitschuh last proposals seem to do the job to me.

I would like to generate Java classes for this pretty big schema: https://github.com/vega/schema/blob/master/vega-lite/v2.0.0-beta.10.json that heavily use the anyOf feature.

@jawadshaik
Copy link

+1 for allOf, oneOf, anyOf support...

@romain3395
Copy link

With XSD, the concept of choice exists also, and when building the Java object based on XSD with JAXB, we get an object with getter and setter of all sub objects. In the java application, you can manage this choice.
If we execute this plugin with a oneOf, the object inside oneOf are not generated and it is the worst solution.

@MaurizioCasciano
Copy link

Any update about this issue? Especially the "allOf" field ?

In my case I'm trying to generate the Java class corresponding to this schema https://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/sdos/attack-pattern.json
however, since all its properties are inside the "allOf" field, nothing is being generated apart from toString, hashCode and equals methods.

@austinmehmet
Copy link

Any update for allOf support?

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

No branches or pull requests