jsonschema2pojo icon indicating copy to clipboard operation
jsonschema2pojo copied to clipboard

Generator generated Empty Java class from perfectly valid Jackson Schema

Open oliTheTM opened this issue 3 years ago • 27 comments
trafficstars

Here is the schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://domain/schema/property.schema.json",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "address",
    "propertyValuation",
    "propertyType",
    "bedrooms",
    "keyWorkerScheme",
    "buyToLet",
    "tenure",
    "floors",
    "country",
    "assetCharges"
  ],
  "properties": {
    "address": {
      "$ref": "https://domain/schema/address.schema.json"
    },
    "propertyValuation": {
      "type": "number"
    },
    "propertyType": {
      "type": "string",
      "enum": [
        "House",
        "Terraced Property",
        "Semi-Detached Property",
        "Detached Property",
        "Flat/Maisonette",
        "End Terrace House",
        "Mid Terrace House",
        "Semi-Detached House",
        "Detached House",
        "Semi-Detached Bungalow",
        "Detached Bungalow",
        "Converted Flat",
        "Purpose Built Flat",
        "Retirement Flat (Not Used)",
        "Bungalow Property",
        "Terraced Bungalow",
        "Town House (Not Used)",
        "End Terrace Bungalow",
        "Mid Terrace Bungalow",
        "End Terrace Property",
        "Mid Terrace Property",
        "Terraced House"
      ]
    },
    "bedrooms": {
      "type": "number"
    },
    "keyWorkerScheme": {
      "type": "boolean"
    },
    "buyToLet": {
      "type": "boolean"
    },
    "tenure": {
      "type": "string",
      "enum": ["Leasehold", "Freehold"]
    },
    "floors": {
      "type": "number"
    },
    "country": {
      "type": "string",
      "enum": ["England", "Scotland", "Wales", "Northern Ireland"]
    },
    "assetCharges": {
      "type": "array",
      "items": {
        "$ref": "https://domain/schema/property-asset-charge.json"
      }
    }
  },
  "if": {
    "properties": { "buyToLet": { "const": true } }
  },
  "then": {
    "properties": {
      "buyToLetType": {
        "type": "string",
        "enum": ["Commercial", "Consumer"]
      }
    },
    "required": ["buyToLetType"],
    "if": {
      "properties": { "buyToLetType" : { "const": "Commercial" } }
    },
    "then": {
      "properties": {
        "selfFunding": {
          "type": "boolean"
        }
      },
      "required": ["selfFunding"]
    }
  }
}

Here's the Java file image:

package domain.model;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Property {


}

Here's my config:

package domain.util;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import com.sun.codemodel.*;
import org.jsonschema2pojo.*;
import org.jsonschema2pojo.rules.RuleFactory;
import javax.validation.constraints.NotNull;


public class GenerateClassFromSchema
{
  private static final String BASE_PATH = "src";
  private static final SchemaMapper schemaMapper;
  private static final JCodeModel schemaTransformer;


  static {
    GenerationConfig config = new DefaultGenerationConfig() {
      @Override
      public boolean isIncludeGeneratedAnnotation() {
        return false;
      }
    };
    schemaMapper = new SchemaMapper(new RuleFactory(
            config, (new Jackson2Annotator(config)),
            (new SchemaStore())
    ), (new SchemaGenerator()));
    schemaTransformer = new JCodeModel();
  }


  private static void addJSONAnnotations(JDefinedClass serializableClass) {
    serializableClass.annotate(com.fasterxml.jackson.annotation.JsonIgnoreProperties.class).
      param("ignoreUnknown",true);
  }
  private static void removeAllSetters(JDefinedClass serializableClass) {
    serializableClass.methods().removeIf((m) ->
      (m.type().name().equals("void") && (!m.params().isEmpty()))
    );
  }
  private static void addJSONCreatorConstructor(JDefinedClass serializableClass) {
    JMethod schemaClassConstructor = serializableClass.constructor(JMod.PUBLIC);
    schemaClassConstructor.annotate(com.fasterxml.jackson.annotation.JsonCreator.class);
    Predicate<String> isRequired = Pattern.compile("required", Pattern.CASE_INSENSITIVE).asPredicate();
    serializableClass.fields().forEach((k, v) -> {
      //@TODO: Assumption that the generator will comment all required fields with "..(Required).."
      String paramName = ('_'+k);
      if (isRequired.test(v.javadoc().toString())) {
        schemaClassConstructor.param(v.type(), paramName).
          annotate(com.fasterxml.jackson.annotation.JsonProperty.class).
          param("value", k).
          param("required", true);
      } else
        schemaClassConstructor.param(v.type(), paramName).
          annotate(com.fasterxml.jackson.annotation.JsonProperty.class).
          param("value", k);
      schemaClassConstructor.body().assign(v, schemaClassConstructor.params().get(schemaClassConstructor.params().size() - 1));
    });
  }

  public static List<String> classFromSchema(@NotNull() String namespace, @NotNull() String className, @NotNull() String schema) {
    JType thePackage = null;
    try {
      thePackage = schemaMapper.generate(schemaTransformer, className, namespace, schema);
    } catch (IOException e) {
      System.err.println(e.getMessage());
    }
    ArrayList<String> classLocations = new ArrayList<String>();
    // 0. Get generated class to modify:
    JClass boxedPackage = thePackage.boxify();
    if (!Objects.isNull(boxedPackage))
      boxedPackage._package().classes().forEachRemaining((c) -> {
        if (c.name().equals(className)) {
          addJSONAnnotations(c);
          addJSONCreatorConstructor(c);
          removeAllSetters(c);
          //Write the class to file using schemaTransformer:
          File schemaFile = new File(BASE_PATH);
          System.out.println("*Registering model*: " + namespace + '.' + className);
          try {
            schemaTransformer.build(schemaFile);
          } catch (IOException e) {
            System.err.println(e.getMessage());
          }
          classLocations.add(
            schemaFile.getAbsolutePath()+'\\'+namespace.replaceAll("\\.", "\\\\")+
            className+".java"
          );
        }
      });
    else
      System.out.println("Could not register model: "+namespace+'.'+className);
    return classLocations;
  }
}

oliTheTM avatar Mar 30 '22 13:03 oliTheTM

False-Negative?

oliTheTM avatar Mar 30 '22 13:03 oliTheTM

This schema seems to work fine for me when I try it at jsonschema2pojo.org, which suggests that you have a problem in your own code.

If you remove all the additional code you have added to modify the classes, and just build the result of schemaMapper.generate, does it work correctly?

joelittlejohn avatar Mar 30 '22 22:03 joelittlejohn

I'll try that now, thanks.

oliTheTM avatar Mar 31 '22 10:03 oliTheTM

Right, so what exactly do you get as an output when you test this in your env? Also, what version of the library are you using?

If I run it just by itself without config I don't even produce a file anymore.. :(

oliTheTM avatar Apr 02 '22 14:04 oliTheTM

@oliTheTM URI's like "https://domain/schema/address.schema.json" are not resolvable and hence were probably omitted by joelittlejohn when attempting to validate the claim.

If those ref's are omitted (there's no possibility to determine their content) jsonschema2pojo ver. 1.1.1 generated output that starts has following content (providing only head and tail of body):

import java.util.HashMap;
import java.util.Map;
import javax.annotation.processing.Generated;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonValue;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
    "propertyValuation",
    "propertyType",
    "bedrooms",
    "keyWorkerScheme",
    "buyToLet",
    "tenure",
    "floors",
    "country"
})
@Generated("jsonschema2pojo")
public class Property {

    /**
     * 
     * (Required)
     * 
     */
    @JsonProperty("propertyValuation")
    private Double propertyValuation;
    /**
     * 
     * (Required)
     * 
     */
    @JsonProperty("propertyType")
    private Property.PropertyType propertyType;

... CODE DELIBERATELY OMITTED TO REDUCE SIZE ...

    @Generated("jsonschema2pojo")
    public enum Tenure {

        LEASEHOLD("Leasehold"),
        FREEHOLD("Freehold");
        private final String value;
        private final static Map<String, Property.Tenure> CONSTANTS = new HashMap<String, Property.Tenure>();

        static {
            for (Property.Tenure c: values()) {
                CONSTANTS.put(c.value, c);
            }
        }

        Tenure(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return this.value;
        }

        @JsonValue
        public String value() {
            return this.value;
        }

        @JsonCreator
        public static Property.Tenure fromValue(String value) {
            Property.Tenure constant = CONSTANTS.get(value);
            if (constant == null) {
                throw new IllegalArgumentException(value);
            } else {
                return constant;
            }
        }

    }

}

So if the claim holds that nothing is generated without providing custom configuration - perhaps the issue is with referenced schemas

unkish avatar Apr 04 '22 08:04 unkish

I have ordered my schemas starting with the Independant ones and then followed by 1st degree dependent ones then 2nd, etc.. I have observed that this works for other entities of my model. Could it be the "if" expression that's part of this schema that's the problem??

I mean, surely it reads the URL as an ID and just pattern-matches with the IDs of pre-processed schemas; no??

oliTheTM avatar Apr 04 '22 12:04 oliTheTM

It didn't turn out to be a problem for me, here's what steps have been taken:

  1. json schema copied from https://github.com/joelittlejohn/jsonschema2pojo/issues/1387#issue-1186444703
  2. following parts removed from json schema file
    "address": {
      "$ref": "https://domain/schema/address.schema.json"
    },
    "assetCharges": {
      "type": "array",
      "items": {
        "$ref": "https://domain/schema/property-asset-charge.json"
      }
    }
  1. following command executed: jsonschema2pojo -s Property.json -t .
  2. copied json schema (with ref's removed from it) to https://www.jsonschema2pojo.org/ 4.a. Selected Source type to be JSON Schema 4.b. Clicked Preview button

unkish avatar Apr 04 '22 16:04 unkish

But isn't it true that the $ref could be either an ID lookup or, if there were a schema-register, an HTTP|GET. Then, in my case it would be a lookup because the $id matches an entity that it parsed before? Right?

What I'm trying to say is, does it not remember what it processed before???

oliTheTM avatar Apr 10 '22 12:04 oliTheTM

It didn't turn out to be a problem for me, here's what steps have been taken:

  1. json schema copied from Generator generated Empty Java class from perfectly valid Jackson Schema #1387 (comment)
  2. following parts removed from json schema file
    "address": {
      "$ref": "https://domain/schema/address.schema.json"
    },
    "assetCharges": {
      "type": "array",
      "items": {
        "$ref": "https://domain/schema/property-asset-charge.json"
      }
    }
  1. following command executed: jsonschema2pojo -s Property.json -t .
  2. copied json schema (with ref's removed from it) to https://www.jsonschema2pojo.org/ 4.a. Selected Source type to be JSON Schema 4.b. Clicked Preview button

I really can't omit those dependencies, sorry. There needs to be another way. Besides, it does actually seem to work in most cases.

oliTheTM avatar Apr 10 '22 12:04 oliTheTM

I assume the answer to my question is Yes. If that is so, what is causing this particular schema to return empty?

You still haven't shown me what is produced on your end?

oliTheTM avatar Apr 10 '22 12:04 oliTheTM

You still haven't shown me what is produced on your end?

Partial output was provided here Also all the steps were provided - and it should be possible to reproduce output.

Without content of $ref's issue wasn't reproduced

unkish avatar Apr 11 '22 13:04 unkish

There is a conditional part to the schema (the "if" components). I would like to see how that got interpreted.

Unfortunately this is omitted from the partial output.

Or if you like you can just tell me how "if" components are generally handled?

oliTheTM avatar Apr 12 '22 13:04 oliTheTM

You still haven't shown me what is produced on your end?

Partial output was provided here Also all the steps were provided - and it should be possible to reproduce output.

Without content of $ref's issue wasn't reproduced

I cannot ignore the $ref's

oliTheTM avatar Apr 12 '22 13:04 oliTheTM

I cannot ignore the $ref's

It's understood as should be understood that without these $ref's being valid/reachable/having meaningful content it's impossible to replicate your issue.

Or if you like you can just tell me how "if" components are generally handled?

It's quite simple - conditional subschemas are not supported they are ignored, to assert that claim we could take an overly simplified version of schema at hand:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://domain/schema/property.schema.json",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "buyToLet"
  ],
  "properties": {
    "buyToLet": {
      "type": "boolean"
    }
  },
  "if": {
    "properties": { "buyToLet": { "const": true } }
  },
  "then": {
    "properties": {
      "buyToLetType": {
        "type": "string",
        "enum": ["Commercial", "Consumer"]
      }
    },
    "required": ["buyToLetType"],
    "if": {
      "properties": { "buyToLetType" : { "const": "Commercial" } }
    },
    "then": {
      "properties": {
        "selfFunding": {
          "type": "boolean"
        }
      },
      "required": ["selfFunding"]
    }
  }
}

Which (jsonschema2pojo.bat -s Property.json -t .) produces following result:

import javax.annotation.processing.Generated;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
    "buyToLet"
})
@Generated("jsonschema2pojo")
public class Property {

    /**
     *
     * (Required)
     *
     */
    @JsonProperty("buyToLet")
    private Boolean buyToLet;

    /**
     *
     * (Required)
     *
     */
    @JsonProperty("buyToLet")
    public Boolean getBuyToLet() {
        return buyToLet;
    }

    /**
     *
     * (Required)
     *
     */
    @JsonProperty("buyToLet")
    public void setBuyToLet(Boolean buyToLet) {
        this.buyToLet = buyToLet;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(Property.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('[');
        sb.append("buyToLet");
        sb.append('=');
        sb.append(((this.buyToLet == null)?"<null>":this.buyToLet));
        sb.append(',');
        if (sb.charAt((sb.length()- 1)) == ',') {
            sb.setCharAt((sb.length()- 1), ']');
        } else {
            sb.append(']');
        }
        return sb.toString();
    }

    @Override
    public int hashCode() {
        int result = 1;
        result = ((result* 31)+((this.buyToLet == null)? 0 :this.buyToLet.hashCode()));
        return result;
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if ((other instanceof Property) == false) {
            return false;
        }
        Property rhs = ((Property) other);
        return ((this.buyToLet == rhs.buyToLet)||((this.buyToLet!= null)&&this.buyToLet.equals(rhs.buyToLet)));
    }

}

unkish avatar Apr 13 '22 06:04 unkish

Is there any configuration I can use (an overload e.g.) that allows me to manage "if" elements?

oliTheTM avatar Apr 13 '22 18:04 oliTheTM

Is there any configuration I can use (an overload e.g.) that allows me to manage "if" elements?

Perhaps overriding RuleFactory::getObjectRule to return extended/customized ObjectRule

unkish avatar Apr 14 '22 06:04 unkish

Is there any configuration I can use (an overload e.g.) that allows me to manage "if" elements?

Perhaps overriding RuleFactory::getObjectRule to return extended/customized ObjectRule

Er... how exactly do you make a custom ObjectRule?

Also, ObjectRule has protected instantiation.

Thanks.

oliTheTM avatar Apr 14 '22 09:04 oliTheTM

There are several strategies, for example:

  • copy whole code of ObjectRule and make adjustments
  • extend ObjectRule and override method calls
  • create ObjectRule wrapper/delegator

unkish avatar Apr 14 '22 10:04 unkish

I can't do the 1st 2 because, as I said, ObjectRule has protected instantiation. A practical example would be nice. Thanks.

oliTheTM avatar Apr 14 '22 10:04 oliTheTM

    static class CustomObjectRule extends ObjectRule {

        public CustomObjectRule(
                RuleFactory ruleFactory,
                ParcelableHelper parcelableHelper,
                ReflectionHelper reflectionHelper) {
            super(ruleFactory, parcelableHelper, reflectionHelper);
        }

        @Override
        public JType apply(String nodeName, JsonNode node, JsonNode parent, JPackage _package, Schema schema) {
            final JType result = super.apply(nodeName, node, parent, _package, schema);
            // custom logic goes here
            return result;
        }
    }

    static {
        GenerationConfig config = new DefaultGenerationConfig() {
            @Override
            public boolean isIncludeGeneratedAnnotation() {
                return false;
            }
        };
        schemaMapper = new SchemaMapper(
                new RuleFactory(
                        config,
                        new Jackson2Annotator(config),
                        new SchemaStore()) {

                    @Override
                    public Rule<JPackage, JType> getObjectRule() {
                        return new CustomObjectRule(this, new ParcelableHelper(), getReflectionHelper());
                    }
                },
                new SchemaGenerator());
        schemaTransformer = new JCodeModel();
    }

unkish avatar Apr 14 '22 12:04 unkish

One more question sorry.

Is this method apply called multiple times during 1 class generation or is it only called once? I assume the former given it has the parameter nodeName.

oliTheTM avatar Apr 17 '22 11:04 oliTheTM

ObjectRule::apply should be called once per object definition.

unkish avatar Apr 18 '22 05:04 unkish

Ah.. so then how would I traverse the Node tree in order to find an "if" ?

oliTheTM avatar Apr 22 '22 14:04 oliTheTM

Ah.. so then how would I traverse the Node tree in order to find an "if" ?

In provided example above you'd be doing it here // custom logic goes here


P.S. Please note that:

  • questions how to handle/implement if/then logic are not related to given topic

  • answer to "False-Negative?" has been provided to the best extent given the limited input set

unkish avatar Apr 25 '22 10:04 unkish

Let me do some things and get back to you as to whether/not there really is an Issue; sorry.

oliTheTM avatar Apr 26 '22 11:04 oliTheTM

No problem.

unkish avatar Apr 26 '22 12:04 unkish

@joelittlejohn @unkish @HanSolo @davidpadbury What do you think of this?\/ It's only possible if I can mute & clone schemas & their components.

[*Premise: jsonSchema2POJO is called on the original schema and the following hook is triggered*]

a. PARSE the Original schema in order to compute (propertyDefinitions : Map<String, (Property : <name,JSONtype,required?,predicate:Function<?, bool>,generator:Function<Random,?>>)>)

b. ENUMERATE the conditional-part of the Original-Schema in order to compute (polymorphisms : List<Polymorphism>)

c. GIVEN propertyDefinitions & polymorphisms..

d. DELETE the "if","then","else" on the original schema

e. COMPLETE the generation of the new class corresponding to the original-schema without ifThenElse

f. ASSIGN the superType property to all elements in polymorphisms as this newly generated type

g. COPY the Original Schema as a Polymorphism schema |polymorphisms| times

h. ForAll schema copies & corresponding polymorphisms (<Si, Pi>):

	h1. Use Pi in order to MUTE Si such that:
		
		h1i. Iff required then ensures existence in "required" JsonNode

		h1ii. The type is polymorphic to JSONtype

	h2. CALL jsonSchema2POJO on Si

	h3. GIVEN another hook in jsonSchema2POJO; it's called only now, then it does the following:

		h3i. Mute the generated-class so that it inherits from Si.superClass (see [f])

		h3ii. Mute the generated-class constructor so that it initially calls super()

		h3iii. Mute said constructor again so that all predicates of Si are asserted else EXCEPTION

	h4. COMPLETE the generation of this sub-class 		

	h5. ASSIGN Si.subType as this new generated class

			[*EVERY OBJECT HERE/\ INTERNAL TO GeneratedClassFactory*]

i. GIVEN all polymorphism Java classes are now generated (and all inherit the super) and are determined by their associated Predicate & Generator..

j. STORE all of this information in GeneratedClassFactory; in order for the following to be applied:

	j1. GeneratedClassFactory::newSample : <Class<T>, Integer>  -->  List<T>

	j2. GeneratedClassFactory::deserialize : <String, Class<T>>  -->  ~T


			[*Since any Polymorphism corresponds to a Test-Case, it follows that newSample knows, ahead of time, which Polymorphism to use*]

oliTheTM avatar May 13 '22 12:05 oliTheTM