jsonschema2pojo
jsonschema2pojo copied to clipboard
Implementation of oneOf support.
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.
-
There is single branch in
oneOf. In this case the branch is treated asextends. -
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)
})
- 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)
})
- 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;
}
}
}
- 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
valueand 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.
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
@ThanksForAllTheFish I am able to work on the contribution, but I need some info from the maintainer (cc @joelittlejohn), before I dedicate time.
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
any news?
any news?
@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?
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;
}
}