jsonschema2pojo icon indicating copy to clipboard operation
jsonschema2pojo copied to clipboard

Feasibility of using oneOf, anyOf or allOf in schema for jsonschema2pojo.

Open lvimalchandran opened this issue 6 years ago • 26 comments

Hi, Can anyone please let me know whether using oneOf, anyOf or allOf in schema for jsonschema2pojo will work or not. Or is there any other option to handle the scenario.

lvimalchandran avatar Feb 05 '18 17:02 lvimalchandran

Have you seen https://github.com/joelittlejohn/jsonschema2pojo/wiki/Proposal-for-allOf,-anyOf-and-oneOf?

ehrmann avatar Feb 14 '18 23:02 ehrmann

@lvimalchandran These features of JSON Schema have a lot of problems when it comes to static binding. They are really geared to validation. In general, it seems like any potential binding solution would require a validator during type selection. That is blocking support for these keywords.

ctrimble avatar Feb 15 '18 07:02 ctrimble

Hi, I read the information here https://github.com/joelittlejohn/jsonschema2pojo/wiki/Proposal-for-allOf,-anyOf-and-oneOf But I do not understand, if the jsonschema2pojo allows the use of simple OnOff? Or the use of these keywords does not work at all?

ilyako87 avatar Aug 08 '18 07:08 ilyako87

Hi, I am working on a project which used to have jsnoschema2pojo Gradle plugin 0.5.1

the spec has the attr: { description: some description, myannoation1: value1, myannoation2: value2, allOf { $ref: anotherType } }

In version 0.5.1 this used to work once I did an upgrade to version 1.0.1 it stopped works.

I use a class to create my own annotations, because without this allOf, as this example

attr: { description: some description, myannoation1: value1, myannoation2: value2, $ref: anotherType }

All that is out of $ref is not used, then my own annotations are not generated

Does anybody have any suggestions?

Is it related to this issue?

tidanilocarvalho avatar Oct 14 '19 19:10 tidanilocarvalho

Any update on using oneOf

AceHack avatar Apr 08 '20 00:04 AceHack

same question as AceHack: Any update on using oneOf's?

andrevocat avatar Apr 09 '20 12:04 andrevocat

I'm curious what the expected Java class would look like in a "oneOf" scenario, since Java doesn't really have support for such a notion. Could one of you post an example of what you'd expect the output to be? Is the problem that you just want the schema generator to generate a class with all candidates of oneOf included in the class?

For example are you expecting this schema

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

to generate this class?

class OutterClass
{
  private A a;
  private B b;
  private C c;
  private D d;
}

Honestly, I'm just struggling with what the expected behavior would have been, and would probably benefit from a test schema and expected output.

dewthefifth avatar Apr 10 '20 12:04 dewthefifth

Hi, just as a first idea not really knowing if it could work

Schema with oneOf "dbParams":{ "oneOf": [ {"$ref": "#/definitions/mongoParams"}, {"$ref": "#/definitions/cassandra.Params"} ] } could give following classes: class DbParams class MongoParams extends DbParams class CassandraParams extends DbParams class OutterClass { private DbParams dbParams; } or use of an empty Interface like: public interface DbParams { } class MongoParams implements DbParams class CassandraParams implements DbParams class OutterClass { private DbParams dbParams; } Hope this helps

andrevocat avatar Apr 13 '20 18:04 andrevocat

@dewthefifth it's not as this. I fead following schema as "include a, or include b, but not both"

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

Your suggestion is more like this:

"properties": {
   "a" :        { "ref": "a.json" },
    "b":    { "ref": "b.json" }
      }

eirnym avatar Apr 15 '20 19:04 eirnym

Sorry for the absence, I ended up sick for a bit.

@eirnym I understand the difference in my suggestion vs the schema, my point was that Java doesn't particularly support the notion of a "oneOf" or "choice". However, I didn't really consider using interfaces the way @andrevocat suggested. I think that adds another perspective that could be explored, albeit a perspective that could be very difficult to work with since it would end up creating a number of generic interfaces. It would need to be interfaces, since you'd be opening an opportunity for MongoParams to be a "oneOf" in any number of places (IE any number of interfaces), and optimally you'd be able to detect if the "oneOf" scenarios matched one another and reuse the interface, but in order to make sense I think it might need to match in terms of choices and in terms of name.

It's something worth considering though.

dewthefifth avatar Apr 23 '20 12:04 dewthefifth

@dewthefifth I glad you're back and it wasn't serious. I'm sorry for my useless comment.

(all below examples are in YAML as it's much easier to type snippets by hands)

Basically it's annoying a bit for me to write every time:

# for generator:
extends:
   $ref: base_class.yaml

# for others:
allOf:
- $ref: base_class.yaml

For basic implementation in conjunction with library to check actual JSON with actual schema, I see following algorithm:

  • All branches which doesn't contain references and/or definitions -> ignore (with an exception of allOf -> merge)
  • I see it as: if there's only one allOf which takes a reference => it's the same as 'extends'
  • everything else -> just copy fields. It's seems to be impossible to tell a random generator and tell it "you should take only these fields" (this library supports many of them)

This will result with a class which extends defined in referenced.yaml, having 2 properties someProperty, someOtherProperty

type: object
allOf:
  - properties:
      someProperty:
        type: string
  - properties:
      someOtherProperty:
        type: integer
  - required:
      - someOtherProperty
  - $ref: referenced.yaml

This will result with a class which extends defined in referenced.yaml, having 2 properties someProperty, someOtherProperty, because you can't decide on this level how it would be (de)-serializing and in which cases which field to take. In Java only nulls are some "border".

type: object
anyOf: # or oneOf as well
  - properties:
      someProperty:
        type: string
  - properties:
      someOtherProperty:
        type: integer
  - required:
      - someOtherProperty
  - $ref: referenced.yaml

eirnym avatar Apr 23 '20 16:04 eirnym

Yes, the way I see it:

  • allOf = merge
  • anyOf and oneOf = generate an interface with a number of implementing classes

There are countless examples where this approach would not accurately express the intent of the schema, but it's better than nothing.

In the case of anyOf/oneOf, Jackson (or any other library) will not be able to deserialize without some kind of type hint. So we would need to annotate with JsonTypeInfo and JsonSubTypes. We require that a sub type name is present in the allOf/anyOf schemas and we would require that a property name is chosen which will identify the 'type' of the JSON object. I'm not really interested in trying to support all possible JsonTypeInfo strategies defined by JsonTypeInfo.As and JsonTypeInfo.Id.

joelittlejohn avatar Apr 23 '20 21:04 joelittlejohn

I prefer JsonTypeInfo.Id.NAME because it's a lot more secure. All the recent Jackson CVEs seem to be problems associated with allowing arbitrary classes to be instantiated with crafted JSON data.

joelittlejohn avatar Apr 23 '20 21:04 joelittlejohn

The subtypes which restrict the types are not part of the CVEs. It's when you don't restrict and allow any class by name where the CVEs come in like enableDefaultTyping.

https://www.google.com/amp/s/blog.sonatype.com/jackson-databind-the-end-of-the-blacklist%3fhs_amp=true

AceHack avatar Apr 24 '20 02:04 AceHack

@joelittlejohn we need to preserve inheritance if possible. Otherwise I'll have to repeat the same code every time I had an inheritance.

PS: we use json validation libraries, so we have to have allOf.

eirnym avatar Apr 24 '20 16:04 eirnym

Any update? Would love oneOf, don't really care about allOf or anyOf?

AceHack avatar Nov 18 '20 20:11 AceHack

This will always be a little funny because JSON Schema was never intended as a representation for a polymorphic class hierarchy, so solutions will either feel bolted on or can only use a subset of JSON Schema.

ehrmann avatar Nov 24 '20 04:11 ehrmann

Hello everyone (by the way, great community job you are all doing here).

I've came across jsonschema2pojo when trying to generate java code from some JSON Schemas that have the infamous (code generation speaking) allOf.

Please correct me if I'm wrong (I will probably be, I've just arrived), but may be we are mixing here schema validation with code generation. Things like additionalProperties should refer to validation which values can it have, not code which attributes the class can have.

Would it make sense to you that allOf, anyOf and oneOf were treated equally when it comes to code generation? Applying the following rules:

  • Every "schema" referenced in a "$ref" of a xxxOf should generate (apart from the class) an interface with the getters and setters defined in the "properties" of the referenced schema
  • Every xxxOf should generate a class that implements all the interfaces defined in the nested schemas and xxxOf's.
  • And at the same time has the "properties" defined in its nested "object" element.

This way you can accommodate every possibility, and it's the validation that should say if the content of the instance is valid or not. To me the structure should accommodate the minimal that contains every possible case, not be valid at compile time.

For instance, the "conflicting cases" mentioned in the wiki:

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

Should generate:

  • 4 interfaces (interfaceA, interfaceB, interfaceC and interfaceD), each one with the getters/setters definied in their respective schemas.
  • 4 classes, each one for each schema (classA implements interfaceA, classB implements interfaceB, etc...). This was already being generated, we would be adding only the implements and @Override's
  • A class that implements this 4 interfaces (this would be the one defined in the conflicting json schema) and @Override's all the getters/setters
  • The naming would clearly be something to work on

I'll try to work on a PR

gmanjon avatar Jan 28 '22 16:01 gmanjon

A first step to work on allOf could be the merging of compatible objects:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
        "MCVE": {
          "allOf": [
            {
              "type": "object",
              "properties": {
                "value": {
                  "type": "number"
                },
                "unit": {
                  "type": "string"
                }
              },
              "required": ["value", "unit"]
            },
            {
              "properties": {
                "unit": {
                  "type": "string",
                  "const": "(unitless)"
                }
              },
              "required": ["unit"]
            }
          ]
        }
    }
}

is equivalent to

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
        "MCVE": {
            "type" : "object",
            "properties" : {
              "value" : {
                "type" : "number"
              },
              "unit" : {
                "type" : "string",
                "const" : "(unitless)"
              }
            },
            "required": ["value", "unit"]
        }
    }
}

and could be parsed accordingly.

viscontem avatar Jun 02 '22 09:06 viscontem

Just throwing an idea, but it could be implemented as base class that has accessors to get the specialized object. e.g. for

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

would end up being:

public class NameMe {
    // include additionalProperties get/set/etc

   public A getAsA() {
     // use A to parse the contents of this object
   }

   public B getAsB() {
    // use B to parse the contents of this object
   }

  public C getAsC() {
    // etc
  }
 
  public D getAsD() {
    //etc
  }
}

It would be responsability of the user to know what object could be used - but maybe this can make use of the actual schema to know such if it's something valid - such as:

public isC() {
  return isValid(rawContent, cSchema);
}

josejulio avatar Sep 09 '22 15:09 josejulio

Hello all, I understand the difficulty of implementing allOf, oneOf, anyOf, at least after reading through this.

I would say there should be made a difference here between code generation and validation. if I have, say, a oneOf { transactionNumber, errorMessage } then I would expect the generated code to have 2 properties, transactionNumber and errorMessage, and I as the implementor are responsible to fill only one; and maybe the fields should have serialize-only-if-not-null annotations or such; a schema validator will tell me if I fail to populate those correctly. A generated code comment indicating the type of "*Of" might also help here.

However, right now, these fields snd structures are completely omitted, and I have to manually patch them into the generated code, field, equals and hashCode.

So my suggestion is to just generate the code, treating the "*Of" just like regular object / properties. (being maybe only a first step, but a big one in contrast to just omitting everything)

teicher avatar Sep 21 '22 11:09 teicher

Since this feature request keeps coming up... JSON Schema was really designed for validation, not representing class schemas with inheritance, and that's why this doesn't quite work. Protobuf and Java records don't support inheritance either, and tend to suggest composition instead. If you really need inheritance, I'd consider a different codegen tool. Otherwise, I'd work with its limitations.

ehrmann avatar Sep 24 '22 05:09 ehrmann

Yes David, I totally agree, the "*Of" should be just ignored, however the structures inside should be generated/included just as if there had been no "*Of" container. This would ensure user could move the properties around using their IDE refactoring function, if they want, vs just missing them completely "without a trace".

teicher avatar Sep 24 '22 10:09 teicher

This is the one thing I need so this tool can be used at all. Right now it generates 2 pojo's out of a large json schema, the second one contains the rest as oneof elements. Got it. I'll need to do it by hand then :/

Frontrider avatar Apr 28 '23 14:04 Frontrider

Could we at least add a warning message in the output noting that *Of is currently not supported (maybe with a link to this issue / the wiki page)? As it is now, the maven-plugin just silently skips *Of, not generating anything in our case. If the plugin would print a corresponding warn message this would likely save many people a lot of time trying to debug why no schemas are being generated.

Elewyth avatar Jul 27 '23 07:07 Elewyth

@Elewyth Yes, agreed, that makes a lot of sense.

joelittlejohn avatar Jul 27 '23 09:07 joelittlejohn