This is an automated email from the ASF dual-hosted git repository.

rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git


The following commit(s) were added to refs/heads/master by this push:
     new 5ca42f7f [JOHNZON-387] basic jsonschema to pojo generation logic
5ca42f7f is described below

commit 5ca42f7f9013b5aaf31ec66803929be35f98152d
Author: Romain Manni-Bucau <rmannibu...@gmail.com>
AuthorDate: Mon Aug 8 11:56:06 2022 +0200

    [JOHNZON-387] basic jsonschema to pojo generation logic
---
 johnzon-jsonschema/pom.xml                         |   2 +-
 .../jsonschema/generator/PojoGenerator.java        | 633 ++++++++++++++++++++
 .../jsonschema/generator/PojoGeneratorTest.java    | 624 ++++++++++++++++++++
 .../src/test/resources/ConfigMap.json              | 193 +++++++
 johnzon-jsonschema/src/test/resources/Node.json    | 634 +++++++++++++++++++++
 .../johnzon/maven/plugin/JsonSchemaToPojoMojo.java | 129 +++++
 pom.xml                                            |   4 +-
 7 files changed, 2216 insertions(+), 3 deletions(-)

diff --git a/johnzon-jsonschema/pom.xml b/johnzon-jsonschema/pom.xml
index a03ba174..38464977 100644
--- a/johnzon-jsonschema/pom.xml
+++ b/johnzon-jsonschema/pom.xml
@@ -32,7 +32,7 @@
     <dependency> <!-- only when nashorn will be dropped -->
       <groupId>org.jruby.joni</groupId>
       <artifactId>joni</artifactId>
-      <version>2.1.16</version>
+      <version>2.1.41</version>
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
diff --git 
a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/PojoGenerator.java
 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/PojoGenerator.java
new file mode 100644
index 00000000..b410e848
--- /dev/null
+++ 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/PojoGenerator.java
@@ -0,0 +1,633 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonschema.generator;
+
+import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.bind.annotation.JsonbProperty;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static java.util.Collections.singletonMap;
+import static java.util.Comparator.comparing;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+// todo: support ref resolution for schema and avoid to generate as much 
classes as attributes
+public class PojoGenerator {
+    private final PojoConfiguration configuration;
+
+    protected final Set<String> imports = new TreeSet<>(String::compareTo);
+    protected final List<Attribute> attributes = new ArrayList<>();
+    protected final Map<String, String> nested = new 
TreeMap<>(String::compareTo);
+
+    public PojoGenerator(final PojoConfiguration configuration) {
+        this.configuration = configuration;
+    }
+
+    public PojoGenerator setNested(final Map<String, String> nested) {
+        this.nested.putAll(nested);
+        return this;
+    }
+
+    public Map<String, String> generate() {
+        final String name = configuration.getPackageName() + '.' + 
configuration.getClassName();
+        final String path = name.replace('.', '/') + ".java";
+        attributes.sort(comparing(a -> a.javaName));
+        if (!attributes.isEmpty()) {
+            imports.add(Objects.class.getName());
+        }
+        final String content = "" +
+                "package " + configuration.getPackageName() + ";\n" +
+                "\n" +
+                (imports.isEmpty() ? "" : imports.stream()
+                        .sorted()
+                        .map(it -> "import " + it + ";")
+                        .collect(joining("\n", "", "\n\n"))) +
+                "public class " + configuration.getClassName() + 
afterClassName() + " {\n" +
+                (attributes.isEmpty() ?
+                        ("" +
+                                "    @Override\n" +
+                                "    public int hashCode() {\n" +
+                                "        return 0;\n" +
+                                "    }\n" +
+                                "\n" +
+                                "    @Override\n" +
+                                "    public boolean equals(final Object other) 
{\n" +
+                                "        return other instanceof " + 
configuration.getClassName() + ";\n" +
+                                "    }\n" +
+                                "}\n") :
+                        (attributes.stream()
+                                .map(a -> "" +
+                                        (configuration.isAddJsonbProperty() && 
!Objects.equals(a.javaName, a.jsonName) ?
+                                                "    @JsonbProperty(\"" + 
a.jsonName.replace("\"", "\\\"") + "\")\n" :
+                                                "") +
+                                        "    private " + a.type + " " + 
a.javaName + ";")
+                                .collect(joining("\n", "", "\n\n")) +
+                                (configuration.isAddAllArgsConstructor() ?
+                                        "    public " + 
configuration.getClassName() + "() {\n" +
+                                                "        // no-op\n" +
+                                                "    }\n" +
+                                                "\n" +
+                                                "    public " + 
configuration.getClassName() + "(" +
+                                                attributes.stream()
+                                                        .map(a -> "final " + 
a.type + " " + a.javaName)
+                                                        .collect(joining(
+                                                                ",\n" + 
IntStream.range(
+                                                                               
 0,
+                                                                               
 "    public (".length() +
+                                                                               
         configuration.getClassName().length())
+                                                                        
.mapToObj(i -> " ")
+                                                                        
.collect(joining()),
+                                                                "",
+                                                                ") {\n" +
+                                                                        "      
  // no-op\n" +
+                                                                        "    
}\n\n")) :
+                                        "") +
+                                attributes.stream()
+                                        .map(a -> {
+                                            final String marker = 
Character.toUpperCase(a.javaName.charAt(0)) + a.javaName.substring(1);
+                                            return "" +
+                                                    "    public " + a.type + " 
get" + Character.toUpperCase(a.javaName.charAt(0)) + a.javaName.substring(1) + 
"() {\n" +
+                                                    "        return " + 
a.javaName + ";\n" +
+                                                    "    }\n" +
+                                                    "\n" +
+                                                    "    public " +
+                                                    
(configuration.isFluentSetters() ? a.type : "void") +
+                                                    " set" + marker + "(final 
" + a.type + " " + a.javaName + ") {\n" +
+                                                    "        this." + 
a.javaName + " = " + a.javaName + ";\n" +
+                                                    
(configuration.isFluentSetters() ? "    return this;\n" : "") +
+                                                    "    }\n" +
+                                                    "";
+                                        })
+                                        .collect(joining("\n", "", "\n")) +
+                                "    @Override\n" +
+                                "    public int hashCode() {\n" +
+                                "        return Objects.hash(\n" +
+                                attributes.stream()
+                                        .map(a -> a.javaName)
+                                        .collect(joining(",\n                
", "                ", ");\n")) +
+                                "    }\n" +
+                                "\n" +
+                                "    @Override\n" +
+                                "    public boolean equals(final Object 
__other) {\n" +
+                                "        if (!(__other instanceof " + 
configuration.getClassName() + ")) {\n" +
+                                "            return false;\n" +
+                                "        }\n" +
+                                "        final " + 
configuration.getClassName() + " __otherCasted = (" + 
configuration.getClassName() + ") __other;\n" +
+                                "        return " + attributes.stream()
+                                .map(a -> a.javaName)
+                                .map(it -> "Objects.equals(" + it + ", 
__otherCasted." + it + ")")
+                                .collect(joining(" &&\n            ")) + ";\n" 
+
+                                "    }\n"
+                        )) +
+                beforeClassEnd() +
+                "}\n";
+        if (nested.isEmpty()) {
+            return singletonMap(path, content);
+        }
+        return Stream.concat(
+                        nested.entrySet().stream(),
+                        Stream.of(new AbstractMap.SimpleImmutableEntry<>(path, 
content)))
+                .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) 
-> a, () -> new TreeMap<>(String::compareTo)));
+    }
+
+    public PojoGenerator visitSchema(final JsonObject schema) {
+        if (!schema.containsKey("properties")) {
+            throw new IllegalArgumentException("Unsupported schema since it 
does not contain any properties: " + schema);
+        }
+
+        final JsonObject properties = getValueAs(schema, "properties", 
JsonObject.class);
+        final JsonValue required = schema.get("required");
+        final List<String> requiredAttrs = required != null && 
required.getValueType() == JsonValue.ValueType.ARRAY ?
+                
required.asJsonArray().stream().map(JsonString.class::cast).map(JsonString::getString).collect(toList())
 :
+                null;
+        attributes.addAll(properties.entrySet().stream()
+                .map(e -> {
+                    final String javaName = toJavaName(e.getKey());
+                    return new Attribute(
+                            javaName, e.getKey(),
+                            asType(javaName, e.getValue().asJsonObject(), 
requiredAttrs != null && requiredAttrs.contains(e.getKey())));
+                })
+                .collect(toList()));
+
+        return this;
+    }
+
+    /**
+     * @param ref the reference to resolve.
+     * @return the reference class name if resolved else null.
+     */
+    protected String onRef(final String ref) {
+        return null; // todo: check if already in nested for ex
+    }
+
+    protected String beforeClassEnd() {
+        return "";
+    }
+
+    protected String afterClassName() {
+        return "";
+    }
+
+    protected String asType(final String javaName, final JsonObject schema, 
final boolean required) {
+        final JsonValue ref = schema.get("ref");
+        if (ref != null && ref.getValueType() == JsonValue.ValueType.STRING) {
+            final String name = onRef(JsonString.class.cast(ref).getString());
+            if (name != null) {
+                return name;
+            }
+        }
+
+        final JsonValue value = schema.get("type");
+        String type;
+        if (value == null) {
+            if (schema.containsKey("properties") || 
schema.containsKey("additionalProperties")) {
+                type = "object";
+            } else if (schema.containsKey("items")) {
+                type = "array";
+            } else { // unknown, don't fail for wrongly written schema
+                imports.add(JsonValue.class.getName());
+                return JsonValue.class.getSimpleName();
+            }
+        } else {
+            type = JsonString.class.cast(value).getString();
+        }
+        final JsonValue formatValue = schema.get("date-time");
+        if (formatValue != null && formatValue.getValueType() == 
JsonValue.ValueType.STRING) {
+            type = JsonString.class.cast(formatValue).getString();
+        }
+
+        switch (type) {
+            case "array":
+                final JsonObject items = getValueAs(schema, "items", 
JsonObject.class);
+                final String itemType = onItemSchema(javaName, items);
+                imports.add(List.class.getName());
+                return List.class.getSimpleName() + "<" + itemType + ">";
+            case "object":
+                return onObjectAttribute(javaName, schema);
+            case "null":
+                imports.add(JsonValue.class.getName());
+                return JsonValue.class.getSimpleName();
+            case "boolean":
+                return required ? "boolean" : "Boolean";
+            case "string":
+                final JsonValue enumList = schema.get("enum");
+                if (enumList != null && enumList.getValueType() == 
JsonValue.ValueType.ARRAY) {
+                    return onEnum(javaName, enumList);
+                }
+                return "String";
+            case "number":
+            case "double": // openapi
+                return required ? "double" : "Double";
+            // openapi types
+            case "int":
+            case "integer":
+                return required ? "int" : "Integer";
+            case "long":
+                return required ? "long" : "Long";
+            case "float":
+                return required ? "float" : "Float";
+            case "date":
+                imports.add(LocalDate.class.getName());
+                return LocalDate.class.getSimpleName();
+            case "duration":
+                imports.add(Duration.class.getName());
+                return Duration.class.getSimpleName();
+            case "date-time":
+            case "dateTime":
+                imports.add(OffsetDateTime.class.getName());
+                return OffsetDateTime.class.getSimpleName();
+            case "time":
+                imports.add(LocalTime.class.getName());
+                return LocalTime.class.getSimpleName();
+            case "byte":
+                return "byte[]";
+            case "uuid":
+            case "hostname":
+            case "idn-hostname":
+            case "email":
+            case "idn-email":
+            case "ipv4":
+            case "ipv6":
+            case "uri":
+            case "uri-reference":
+            case "iri":
+            case "iri-reference":
+            case "uri-template":
+            case "json-pointer":
+            case "relative-json-pointer":
+            case "regex":
+            case "binary": // base64
+            case "password":
+                return "String";
+            default:
+                throw new IllegalArgumentException("Unsupported type: " + 
type);
+        }
+    }
+
+    protected String onEnum(final String javaName, final JsonValue enumList) {
+        final String className = configuration.getClassName() + 
Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
+        final Map<String, String> values = enumList.asJsonArray().stream()
+                .map(JsonString.class::cast)
+                .map(JsonString::getString)
+                .collect(toMap(identity(), this::toJavaName));
+        final boolean injectValues = !values.keySet().equals(new 
HashSet<>(values.values())); // java != json
+        nested.put(
+                configuration.getPackageName().replace('.', '/') + '/' + 
className + ".java", "" +
+                        "package " + configuration.getPackageName() + ";\n" +
+                        "\n" +
+                        "public enum " + className + " {\n" +
+                        values.entrySet().stream()
+                                .map(it -> "" +
+                                        (injectValues && 
configuration.isAddJsonbProperty() ?
+                                                "    @JsonbProperty(\"" + 
it.getKey().replace("\"", "\\\"") + "\")\n" :
+                                                "") +
+                                        "    " + it.getValue() +
+                                        (injectValues ? "(\"" + 
it.getKey().replace("\"", "\\\"") + "\")" : ""))
+                                .collect(joining(",\n", "", injectValues ? 
";\n\n" : "\n")) +
+                        (injectValues ?
+                                "" +
+                                        "    private String value;\n" +
+                                        "\n" +
+                                        "    " + className + "(final String 
value) {\n" +
+                                        "        this.value = value;\n" +
+                                        "    }\n" +
+                                        "\n" +
+                                        "    public String toString() {\n" +
+                                        "        return value;\n" +
+                                        "    }\n" +
+                                        "" :
+                                "") +
+                        "}\n");
+        return className;
+    }
+
+    protected String onObjectAttribute(final String javaName, final JsonObject 
schema) {
+        final JsonValue additionalProperties = 
schema.get("additionalProperties");
+        final JsonValue properties = schema.get("properties");
+        final boolean hasProperties = properties != null && 
properties.getValueType() == JsonValue.ValueType.OBJECT;
+        if (!hasProperties &&
+                additionalProperties != null &&
+                additionalProperties.getValueType() == 
JsonValue.ValueType.OBJECT) {
+            final JsonObject propSchema = additionalProperties.asJsonObject();
+            final JsonValue propTypeValue = propSchema.get("type");
+            if (propTypeValue != null && propTypeValue.getValueType() == 
JsonValue.ValueType.STRING) {
+                String propType = 
JsonString.class.cast(propTypeValue).getString();
+                final JsonValue formatValue = schema.get("date-time");
+                if (formatValue != null && formatValue.getValueType() == 
JsonValue.ValueType.STRING) {
+                    propType = JsonString.class.cast(formatValue).getString();
+                }
+                switch (propType) {
+                    case "uuid":
+                    case "hostname":
+                    case "idn-hostname":
+                    case "email":
+                    case "idn-email":
+                    case "ipv4":
+                    case "ipv6":
+                    case "uri":
+                    case "uri-reference":
+                    case "iri":
+                    case "iri-reference":
+                    case "uri-template":
+                    case "json-pointer":
+                    case "relative-json-pointer":
+                    case "regex":
+                    case "string":
+                    case "binary":
+                    case "password":
+                        imports.add(Map.class.getName());
+                        return "Map<String, String>";
+                    case "boolean":
+                        imports.add(Map.class.getName());
+                        return "Map<String, Boolean>";
+                    case "number":
+                    case "double":
+                        imports.add(Map.class.getName());
+                        return "Map<String, Double>";
+                    case "int":
+                    case "integer":
+                        imports.add(Map.class.getName());
+                        return "Map<String, Integer>";
+                    case "long":
+                        imports.add(Map.class.getName());
+                        return "Map<String, Long>";
+                    case "float":
+                        imports.add(Map.class.getName());
+                        return "Map<String, Float>";
+                    case "date":
+                        imports.add(Map.class.getName());
+                        imports.add(LocalDate.class.getName());
+                        return "Map<String, LocalDate>";
+                    case "dateTime":
+                    case "date-time":
+                        imports.add(Map.class.getName());
+                        imports.add(OffsetDateTime.class.getName());
+                        return "Map<String, OffsetDateTime>";
+                    case "duration":
+                        imports.add(Map.class.getName());
+                        imports.add(Duration.class.getName());
+                        return "Map<String, Duration>";
+                    case "time":
+                        imports.add(Map.class.getName());
+                        imports.add(LocalTime.class.getName());
+                        return "Map<String, LocalTime>";
+                    default:
+                        // todo: case array, object
+                }
+            }
+        } else if (hasProperties) {
+            final String className = configuration.getClassName() + 
Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
+            nested.putAll(new PojoGenerator(new PojoConfiguration()
+                    .setPackageName(configuration.getPackageName())
+                    .setClassName(className)
+                    .setAddJsonbProperty(configuration.isAddJsonbProperty())
+                    
.setAddAllArgsConstructor(configuration.isAddAllArgsConstructor()))
+                    .visitSchema(schema)
+                    .generate());
+            return className;
+        }
+
+        imports.add(JsonObject.class.getName());
+        return JsonObject.class.getSimpleName();
+    }
+
+    protected String onItemSchema(final String javaName, final JsonObject 
schema) {
+        final JsonValue ref = schema.get("ref");
+        if (ref != null && ref.getValueType() == JsonValue.ValueType.STRING) {
+            final String name = onRef(JsonString.class.cast(ref).getString());
+            if (name != null) {
+                return name;
+            }
+        }
+
+        final JsonValue propTypeValue = schema.get("type");
+        if (propTypeValue != null && propTypeValue.getValueType() == 
JsonValue.ValueType.STRING) {
+            String type = JsonString.class.cast(propTypeValue).getString();
+            final JsonValue formatValue = schema.get("date-time");
+            if (formatValue != null && formatValue.getValueType() == 
JsonValue.ValueType.STRING) {
+                type = JsonString.class.cast(formatValue).getString();
+            }
+            switch (type) {
+                case "array":
+                    throw new IllegalStateException("Array of array 
unsupported");
+                case "object":
+                    final String className = configuration.getClassName() + 
Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
+                    nested.putAll(new PojoGenerator(new PojoConfiguration()
+                            .setPackageName(configuration.getPackageName())
+                            .setClassName(className)
+                            
.setAddJsonbProperty(configuration.isAddJsonbProperty())
+                            
.setAddAllArgsConstructor(configuration.isAddAllArgsConstructor()))
+                            .visitSchema(schema)
+                            .generate());
+                    return className;
+                case "null":
+                    imports.add(JsonValue.class.getName());
+                    return JsonValue.class.getSimpleName();
+                case "boolean":
+                    return "Boolean";
+                case "uuid":
+                case "hostname":
+                case "idn-hostname":
+                case "email":
+                case "idn-email":
+                case "ipv4":
+                case "ipv6":
+                case "uri":
+                case "uri-reference":
+                case "iri":
+                case "iri-reference":
+                case "uri-template":
+                case "json-pointer":
+                case "relative-json-pointer":
+                case "regex":
+                case "string":
+                case "binary":
+                case "password":
+                    return "String";
+                case "number":
+                case "double":
+                    return "Double";
+                case "int":
+                case "integer":
+                    return "Integer";
+                case "long":
+                    return "Long";
+                case "float":
+                    return "Float";
+                case "date":
+                    imports.add(LocalDate.class.getName());
+                    return LocalDate.class.getSimpleName();
+                case "dateTime":
+                    imports.add(OffsetDateTime.class.getName());
+                    return OffsetDateTime.class.getSimpleName();
+                case "duration":
+                    imports.add(Duration.class.getName());
+                    return Duration.class.getSimpleName();
+                case "time":
+                    imports.add(LocalTime.class.getName());
+                    return LocalTime.class.getSimpleName();
+                case "byte":
+                    return "byte[]";
+                default:
+                    throw new IllegalArgumentException("Unsupported type: " + 
type);
+            }
+        }
+
+        imports.add(JsonValue.class.getName());
+        return JsonValue.class.getSimpleName();
+    }
+
+    private String toJavaName(final String key) {
+        String name = key.chars()
+                .mapToObj(i -> 
Character.toString(!Character.isJavaIdentifierPart(i) ? '_' : (char) i))
+                .collect(joining());
+        if (Character.isDigit(name.charAt(0))) {
+            name = "a" + name;
+        }
+        while (name.startsWith("_")) {
+            name = name.substring(1);
+        }
+        if (name.isEmpty()) {
+            throw new IllegalArgumentException("Can't find a name for '" + key 
+ "'");
+        }
+
+        if (isReserved(name)) {
+            name += "Value";
+        }
+
+        if (!Objects.equals(key, name) && configuration.isAddJsonbProperty()) {
+            imports.add(JsonbProperty.class.getName());
+        }
+        return name;
+    }
+
+    protected boolean isReserved(final String name) {
+        return "continue".equals(name) || "break".equals(name) ||
+                "do".equals(name) || "while".equals(name) ||
+                "for".equals(name) ||
+                "if".equals(name) || "else".equals(name) ||
+                "int".equals(name) ||
+                "long".equals(name) ||
+                "float".equals(name) ||
+                "double".equals(name) ||
+                "boolean".equals(name) ||
+                "byte".equals(name) ||
+                "char".equals(name) ||
+                "short".equals(name) ||
+                "String".equals(name);
+    }
+
+    private static <T> T getValueAs(final JsonObject schema, final String 
attribute, final Class<T> type) {
+        final JsonValue value = schema.get(attribute);
+        if (value == null) {
+            throw new IllegalArgumentException("No \"" + attribute + "\" value 
in " + schema);
+        }
+        return valueAs(schema, type, value);
+    }
+
+    private static <T> T valueAs(final JsonObject schema, final Class<T> type, 
final JsonValue value) {
+        if (!type.isInstance(value)) {
+            throw new IllegalArgumentException("\"items\" not an object: " + 
schema);
+        }
+        return type.cast(value);
+    }
+
+    public static class PojoConfiguration {
+        private String packageName = "org.apache.johnzon.generated.pojo";
+        private String className;
+        private boolean addJsonbProperty = true;
+        private boolean addAllArgsConstructor = true;
+        private boolean fluentSetters = false;
+
+        public boolean isFluentSetters() {
+            return fluentSetters;
+        }
+
+        public PojoConfiguration setFluentSetters(final boolean fluentSetters) 
{
+            this.fluentSetters = fluentSetters;
+            return this;
+        }
+
+        public boolean isAddAllArgsConstructor() {
+            return addAllArgsConstructor;
+        }
+
+        public PojoConfiguration setAddAllArgsConstructor(final boolean 
addAllArgsConstructor) {
+            this.addAllArgsConstructor = addAllArgsConstructor;
+            return this;
+        }
+
+        public boolean isAddJsonbProperty() {
+            return addJsonbProperty;
+        }
+
+        public PojoConfiguration setAddJsonbProperty(final boolean 
addJsonbProperty) {
+            this.addJsonbProperty = addJsonbProperty;
+            return this;
+        }
+
+        public String getClassName() {
+            return className;
+        }
+
+        public PojoConfiguration setClassName(final String className) {
+            this.className = className;
+            return this;
+        }
+
+        public String getPackageName() {
+            return packageName;
+        }
+
+        public PojoConfiguration setPackageName(final String packageName) {
+            this.packageName = packageName;
+            return this;
+        }
+    }
+
+    protected static class Attribute {
+        private final String javaName;
+        private final String jsonName;
+        private final String type;
+
+        protected Attribute(final String javaName, final String jsonName, 
final String type) {
+            this.javaName = javaName;
+            this.jsonName = jsonName;
+            this.type = type;
+        }
+    }
+}
diff --git 
a/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/generator/PojoGeneratorTest.java
 
b/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/generator/PojoGeneratorTest.java
new file mode 100644
index 00000000..8115839e
--- /dev/null
+++ 
b/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/generator/PojoGeneratorTest.java
@@ -0,0 +1,624 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonschema.generator;
+
+import org.junit.Test;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+import static org.junit.Assert.assertEquals;
+
+public class PojoGeneratorTest {
+    private final PojoGenerator.PojoConfiguration configuration = new 
PojoGenerator.PojoConfiguration()
+            .setAddAllArgsConstructor(false)
+            .setClassName("TheClass")
+            .setPackageName("org.test");
+
+    @Test
+    public void generate() {
+        final Map<String, String> expected = new HashMap<>();
+        expected.put("org/test/TheClass.java", root());
+        expected.put("org/test/TheClassMetadata.java", metadata());
+        expected.put("org/test/TheClassMetadataManagedFields.java", 
managedFields());
+        expected.put("org/test/TheClassMetadataOwnerReferences.java", 
ownerRefs());
+        final Map<String, String> generated = new PojoGenerator(configuration)
+                .visitSchema(load("ConfigMap.json"))
+                .generate();
+
+        // actual assertion but since we want a better error message we split 
it in 2
+        // assertEquals(expected, generated);
+
+        assertEquals(expected.keySet(), generated.keySet());
+        expected.forEach((k, v) -> assertEquals(v, generated.get(k)));
+    }
+
+    @Test
+    public void generateEnums() {
+        final Map<String, String> generated = new PojoGenerator(configuration)
+                .visitSchema(load("Node.json"))
+                .generate();
+        assertEquals("" +
+                        "package org.test;\n" +
+                        "\n" +
+                        "public enum TheClassSpecTaintsEffect {\n" +
+                        "    PreferNoSchedule,\n" +
+                        "    NoSchedule,\n" +
+                        "    NoExecute\n" +
+                        "}\n",
+                generated.get("org/test/TheClassSpecTaintsEffect.java"));
+        assertEquals("" +
+                        "package org.test;\n" +
+                        "\n" +
+                        "import java.util.Objects;\n" +
+                        "\n" +
+                        "public class TheClassSpecTaints {\n" +
+                        "    private TheClassSpecTaintsEffect effect;\n" +
+                        "    private String key;\n" +
+                        "    private String timeAdded;\n" +
+                        "    private String value;\n" +
+                        "\n" +
+                        "    public TheClassSpecTaintsEffect getEffect() {\n" +
+                        "        return effect;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public void setEffect(final 
TheClassSpecTaintsEffect effect) {\n" +
+                        "        this.effect = effect;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public String getKey() {\n" +
+                        "        return key;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public void setKey(final String key) {\n" +
+                        "        this.key = key;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public String getTimeAdded() {\n" +
+                        "        return timeAdded;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public void setTimeAdded(final String timeAdded) 
{\n" +
+                        "        this.timeAdded = timeAdded;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public String getValue() {\n" +
+                        "        return value;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    public void setValue(final String value) {\n" +
+                        "        this.value = value;\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    @Override\n" +
+                        "    public int hashCode() {\n" +
+                        "        return Objects.hash(\n" +
+                        "                effect,\n" +
+                        "                key,\n" +
+                        "                timeAdded,\n" +
+                        "                value);\n" +
+                        "    }\n" +
+                        "\n" +
+                        "    @Override\n" +
+                        "    public boolean equals(final Object __other) {\n" +
+                        "        if (!(__other instanceof TheClassSpecTaints)) 
{\n" +
+                        "            return false;\n" +
+                        "        }\n" +
+                        "        final TheClassSpecTaints __otherCasted = 
(TheClassSpecTaints) __other;\n" +
+                        "        return Objects.equals(effect, 
__otherCasted.effect) &&\n" +
+                        "            Objects.equals(key, __otherCasted.key) 
&&\n" +
+                        "            Objects.equals(timeAdded, 
__otherCasted.timeAdded) &&\n" +
+                        "            Objects.equals(value, 
__otherCasted.value);\n" +
+                        "    }\n" +
+                        "}\n",
+                generated.get("org/test/TheClassSpecTaints.java"));
+    }
+
+    private static String root() {
+        return "" +
+                "package org.test;\n" +
+                "\n" +
+                "import java.util.Map;\n" +
+                "import java.util.Objects;\n" +
+                "\n" +
+                "public class TheClass {\n" +
+                "    private String apiVersion;\n" +
+                "    private Map<String, String> binaryData;\n" +
+                "    private Map<String, String> data;\n" +
+                "    private Boolean immutable;\n" +
+                "    private String kind;\n" +
+                "    private TheClassMetadata metadata;\n" +
+                "\n" +
+                "    public String getApiVersion() {\n" +
+                "        return apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setApiVersion(final String apiVersion) {\n" +
+                "        this.apiVersion = apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Map<String, String> getBinaryData() {\n" +
+                "        return binaryData;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setBinaryData(final Map<String, String> 
binaryData) {\n" +
+                "        this.binaryData = binaryData;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Map<String, String> getData() {\n" +
+                "        return data;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setData(final Map<String, String> data) {\n" +
+                "        this.data = data;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Boolean getImmutable() {\n" +
+                "        return immutable;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setImmutable(final Boolean immutable) {\n" +
+                "        this.immutable = immutable;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getKind() {\n" +
+                "        return kind;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setKind(final String kind) {\n" +
+                "        this.kind = kind;\n" +
+                "    }\n" +
+                "\n" +
+                "    public TheClassMetadata getMetadata() {\n" +
+                "        return metadata;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setMetadata(final TheClassMetadata metadata) 
{\n" +
+                "        this.metadata = metadata;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public int hashCode() {\n" +
+                "        return Objects.hash(\n" +
+                "                apiVersion,\n" +
+                "                binaryData,\n" +
+                "                data,\n" +
+                "                immutable,\n" +
+                "                kind,\n" +
+                "                metadata);\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public boolean equals(final Object __other) {\n" +
+                "        if (!(__other instanceof TheClass)) {\n" +
+                "            return false;\n" +
+                "        }\n" +
+                "        final TheClass __otherCasted = (TheClass) __other;\n" 
+
+                "        return Objects.equals(apiVersion, 
__otherCasted.apiVersion) &&\n" +
+                "            Objects.equals(binaryData, 
__otherCasted.binaryData) &&\n" +
+                "            Objects.equals(data, __otherCasted.data) &&\n" +
+                "            Objects.equals(immutable, 
__otherCasted.immutable) &&\n" +
+                "            Objects.equals(kind, __otherCasted.kind) &&\n" +
+                "            Objects.equals(metadata, 
__otherCasted.metadata);\n" +
+                "    }\n" +
+                "}\n";
+    }
+
+    private static String metadata() {
+        return "" +
+                "package org.test;\n" +
+                "\n" +
+                "import java.util.List;\n" +
+                "import java.util.Map;\n" +
+                "import java.util.Objects;\n" +
+                "\n" +
+                "public class TheClassMetadata {\n" +
+                "    private Map<String, String> annotations;\n" +
+                "    private String clusterName;\n" +
+                "    private String creationTimestamp;\n" +
+                "    private Integer deletionGracePeriodSeconds;\n" +
+                "    private String deletionTimestamp;\n" +
+                "    private List<String> finalizers;\n" +
+                "    private String generateName;\n" +
+                "    private Integer generation;\n" +
+                "    private Map<String, String> labels;\n" +
+                "    private List<TheClassMetadataManagedFields> 
managedFields;\n" +
+                "    private String name;\n" +
+                "    private String namespace;\n" +
+                "    private List<TheClassMetadataOwnerReferences> 
ownerReferences;\n" +
+                "    private String resourceVersion;\n" +
+                "    private String selfLink;\n" +
+                "    private String uid;\n" +
+                "\n" +
+                "    public Map<String, String> getAnnotations() {\n" +
+                "        return annotations;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setAnnotations(final Map<String, String> 
annotations) {\n" +
+                "        this.annotations = annotations;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getClusterName() {\n" +
+                "        return clusterName;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setClusterName(final String clusterName) {\n" 
+
+                "        this.clusterName = clusterName;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getCreationTimestamp() {\n" +
+                "        return creationTimestamp;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setCreationTimestamp(final String 
creationTimestamp) {\n" +
+                "        this.creationTimestamp = creationTimestamp;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Integer getDeletionGracePeriodSeconds() {\n" +
+                "        return deletionGracePeriodSeconds;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setDeletionGracePeriodSeconds(final Integer 
deletionGracePeriodSeconds) {\n" +
+                "        this.deletionGracePeriodSeconds = 
deletionGracePeriodSeconds;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getDeletionTimestamp() {\n" +
+                "        return deletionTimestamp;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setDeletionTimestamp(final String 
deletionTimestamp) {\n" +
+                "        this.deletionTimestamp = deletionTimestamp;\n" +
+                "    }\n" +
+                "\n" +
+                "    public List<String> getFinalizers() {\n" +
+                "        return finalizers;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setFinalizers(final List<String> finalizers) 
{\n" +
+                "        this.finalizers = finalizers;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getGenerateName() {\n" +
+                "        return generateName;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setGenerateName(final String generateName) 
{\n" +
+                "        this.generateName = generateName;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Integer getGeneration() {\n" +
+                "        return generation;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setGeneration(final Integer generation) {\n" +
+                "        this.generation = generation;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Map<String, String> getLabels() {\n" +
+                "        return labels;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setLabels(final Map<String, String> labels) 
{\n" +
+                "        this.labels = labels;\n" +
+                "    }\n" +
+                "\n" +
+                "    public List<TheClassMetadataManagedFields> 
getManagedFields() {\n" +
+                "        return managedFields;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setManagedFields(final 
List<TheClassMetadataManagedFields> managedFields) {\n" +
+                "        this.managedFields = managedFields;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getName() {\n" +
+                "        return name;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setName(final String name) {\n" +
+                "        this.name = name;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getNamespace() {\n" +
+                "        return namespace;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setNamespace(final String namespace) {\n" +
+                "        this.namespace = namespace;\n" +
+                "    }\n" +
+                "\n" +
+                "    public List<TheClassMetadataOwnerReferences> 
getOwnerReferences() {\n" +
+                "        return ownerReferences;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setOwnerReferences(final 
List<TheClassMetadataOwnerReferences> ownerReferences) {\n" +
+                "        this.ownerReferences = ownerReferences;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getResourceVersion() {\n" +
+                "        return resourceVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setResourceVersion(final String 
resourceVersion) {\n" +
+                "        this.resourceVersion = resourceVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getSelfLink() {\n" +
+                "        return selfLink;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setSelfLink(final String selfLink) {\n" +
+                "        this.selfLink = selfLink;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getUid() {\n" +
+                "        return uid;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setUid(final String uid) {\n" +
+                "        this.uid = uid;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public int hashCode() {\n" +
+                "        return Objects.hash(\n" +
+                "                annotations,\n" +
+                "                clusterName,\n" +
+                "                creationTimestamp,\n" +
+                "                deletionGracePeriodSeconds,\n" +
+                "                deletionTimestamp,\n" +
+                "                finalizers,\n" +
+                "                generateName,\n" +
+                "                generation,\n" +
+                "                labels,\n" +
+                "                managedFields,\n" +
+                "                name,\n" +
+                "                namespace,\n" +
+                "                ownerReferences,\n" +
+                "                resourceVersion,\n" +
+                "                selfLink,\n" +
+                "                uid);\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public boolean equals(final Object __other) {\n" +
+                "        if (!(__other instanceof TheClassMetadata)) {\n" +
+                "            return false;\n" +
+                "        }\n" +
+                "        final TheClassMetadata __otherCasted = 
(TheClassMetadata) __other;\n" +
+                "        return Objects.equals(annotations, 
__otherCasted.annotations) &&\n" +
+                "            Objects.equals(clusterName, 
__otherCasted.clusterName) &&\n" +
+                "            Objects.equals(creationTimestamp, 
__otherCasted.creationTimestamp) &&\n" +
+                "            Objects.equals(deletionGracePeriodSeconds, 
__otherCasted.deletionGracePeriodSeconds) &&\n" +
+                "            Objects.equals(deletionTimestamp, 
__otherCasted.deletionTimestamp) &&\n" +
+                "            Objects.equals(finalizers, 
__otherCasted.finalizers) &&\n" +
+                "            Objects.equals(generateName, 
__otherCasted.generateName) &&\n" +
+                "            Objects.equals(generation, 
__otherCasted.generation) &&\n" +
+                "            Objects.equals(labels, __otherCasted.labels) 
&&\n" +
+                "            Objects.equals(managedFields, 
__otherCasted.managedFields) &&\n" +
+                "            Objects.equals(name, __otherCasted.name) &&\n" +
+                "            Objects.equals(namespace, 
__otherCasted.namespace) &&\n" +
+                "            Objects.equals(ownerReferences, 
__otherCasted.ownerReferences) &&\n" +
+                "            Objects.equals(resourceVersion, 
__otherCasted.resourceVersion) &&\n" +
+                "            Objects.equals(selfLink, __otherCasted.selfLink) 
&&\n" +
+                "            Objects.equals(uid, __otherCasted.uid);\n" +
+                "    }\n" +
+                "}\n";
+    }
+
+    private static String managedFields() {
+        return "" +
+                "package org.test;\n" +
+                "\n" +
+                "import java.util.Objects;\n" +
+                "import javax.json.JsonObject;\n" +
+                "\n" +
+                "public class TheClassMetadataManagedFields {\n" +
+                "    private String apiVersion;\n" +
+                "    private String fieldsType;\n" +
+                "    private JsonObject fieldsV1;\n" +
+                "    private String manager;\n" +
+                "    private String operation;\n" +
+                "    private String subresource;\n" +
+                "    private String time;\n" +
+                "\n" +
+                "    public String getApiVersion() {\n" +
+                "        return apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setApiVersion(final String apiVersion) {\n" +
+                "        this.apiVersion = apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getFieldsType() {\n" +
+                "        return fieldsType;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setFieldsType(final String fieldsType) {\n" +
+                "        this.fieldsType = fieldsType;\n" +
+                "    }\n" +
+                "\n" +
+                "    public JsonObject getFieldsV1() {\n" +
+                "        return fieldsV1;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setFieldsV1(final JsonObject fieldsV1) {\n" +
+                "        this.fieldsV1 = fieldsV1;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getManager() {\n" +
+                "        return manager;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setManager(final String manager) {\n" +
+                "        this.manager = manager;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getOperation() {\n" +
+                "        return operation;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setOperation(final String operation) {\n" +
+                "        this.operation = operation;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getSubresource() {\n" +
+                "        return subresource;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setSubresource(final String subresource) {\n" 
+
+                "        this.subresource = subresource;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getTime() {\n" +
+                "        return time;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setTime(final String time) {\n" +
+                "        this.time = time;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public int hashCode() {\n" +
+                "        return Objects.hash(\n" +
+                "                apiVersion,\n" +
+                "                fieldsType,\n" +
+                "                fieldsV1,\n" +
+                "                manager,\n" +
+                "                operation,\n" +
+                "                subresource,\n" +
+                "                time);\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public boolean equals(final Object __other) {\n" +
+                "        if (!(__other instanceof 
TheClassMetadataManagedFields)) {\n" +
+                "            return false;\n" +
+                "        }\n" +
+                "        final TheClassMetadataManagedFields __otherCasted = 
(TheClassMetadataManagedFields) __other;\n" +
+                "        return Objects.equals(apiVersion, 
__otherCasted.apiVersion) &&\n" +
+                "            Objects.equals(fieldsType, 
__otherCasted.fieldsType) &&\n" +
+                "            Objects.equals(fieldsV1, __otherCasted.fieldsV1) 
&&\n" +
+                "            Objects.equals(manager, __otherCasted.manager) 
&&\n" +
+                "            Objects.equals(operation, 
__otherCasted.operation) &&\n" +
+                "            Objects.equals(subresource, 
__otherCasted.subresource) &&\n" +
+                "            Objects.equals(time, __otherCasted.time);\n" +
+                "    }\n" +
+                "}\n";
+    }
+
+    private static String ownerRefs() {
+        return "" +
+                "package org.test;\n" +
+                "\n" +
+                "import java.util.Objects;\n" +
+                "\n" +
+                "public class TheClassMetadataOwnerReferences {\n" +
+                "    private String apiVersion;\n" +
+                "    private Boolean blockOwnerDeletion;\n" +
+                "    private Boolean controller;\n" +
+                "    private String kind;\n" +
+                "    private String name;\n" +
+                "    private String uid;\n" +
+                "\n" +
+                "    public String getApiVersion() {\n" +
+                "        return apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setApiVersion(final String apiVersion) {\n" +
+                "        this.apiVersion = apiVersion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Boolean getBlockOwnerDeletion() {\n" +
+                "        return blockOwnerDeletion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setBlockOwnerDeletion(final Boolean 
blockOwnerDeletion) {\n" +
+                "        this.blockOwnerDeletion = blockOwnerDeletion;\n" +
+                "    }\n" +
+                "\n" +
+                "    public Boolean getController() {\n" +
+                "        return controller;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setController(final Boolean controller) {\n" +
+                "        this.controller = controller;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getKind() {\n" +
+                "        return kind;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setKind(final String kind) {\n" +
+                "        this.kind = kind;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getName() {\n" +
+                "        return name;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setName(final String name) {\n" +
+                "        this.name = name;\n" +
+                "    }\n" +
+                "\n" +
+                "    public String getUid() {\n" +
+                "        return uid;\n" +
+                "    }\n" +
+                "\n" +
+                "    public void setUid(final String uid) {\n" +
+                "        this.uid = uid;\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public int hashCode() {\n" +
+                "        return Objects.hash(\n" +
+                "                apiVersion,\n" +
+                "                blockOwnerDeletion,\n" +
+                "                controller,\n" +
+                "                kind,\n" +
+                "                name,\n" +
+                "                uid);\n" +
+                "    }\n" +
+                "\n" +
+                "    @Override\n" +
+                "    public boolean equals(final Object __other) {\n" +
+                "        if (!(__other instanceof 
TheClassMetadataOwnerReferences)) {\n" +
+                "            return false;\n" +
+                "        }\n" +
+                "        final TheClassMetadataOwnerReferences __otherCasted = 
(TheClassMetadataOwnerReferences) __other;\n" +
+                "        return Objects.equals(apiVersion, 
__otherCasted.apiVersion) &&\n" +
+                "            Objects.equals(blockOwnerDeletion, 
__otherCasted.blockOwnerDeletion) &&\n" +
+                "            Objects.equals(controller, 
__otherCasted.controller) &&\n" +
+                "            Objects.equals(kind, __otherCasted.kind) &&\n" +
+                "            Objects.equals(name, __otherCasted.name) &&\n" +
+                "            Objects.equals(uid, __otherCasted.uid);\n" +
+                "    }\n" +
+                "}\n";
+    }
+
+    private JsonObject load(final String resource) {
+        try (final JsonReader reader = 
Json.createReader(requireNonNull(Thread.currentThread().getContextClassLoader()
+                .getResourceAsStream(resource)))) {
+            return reader.readObject();
+        }
+    }
+}
diff --git a/johnzon-jsonschema/src/test/resources/ConfigMap.json 
b/johnzon-jsonschema/src/test/resources/ConfigMap.json
new file mode 100644
index 00000000..55385fd1
--- /dev/null
+++ b/johnzon-jsonschema/src/test/resources/ConfigMap.json
@@ -0,0 +1,193 @@
+{
+  "properties": {
+    "apiVersion": {
+      "description": "APIVersion defines the versioned schema of this 
representation of an object. Servers should convert recognized schemas to the 
latest internal value, and may reject unrecognized values. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources";,
+      "type": "string"
+    },
+    "binaryData": {
+      "additionalProperties": {
+        "format": "byte",
+        "type": "string"
+      },
+      "description": "BinaryData contains the binary data. Each key must 
consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain 
byte sequences that are not in the UTF-8 range. The keys stored in BinaryData 
must not overlap with the ones in the Data field, this is enforced during 
validation process. Using this field will require 1.10+ apiserver and kubelet.",
+      "type": "object"
+    },
+    "data": {
+      "additionalProperties": {
+        "type": "string"
+      },
+      "description": "Data contains the configuration data. Each key must 
consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte 
sequences must use the BinaryData field. The keys stored in Data must not 
overlap with the keys in the BinaryData field, this is enforced during 
validation process.",
+      "type": "object"
+    },
+    "immutable": {
+      "description": "Immutable, if set to true, ensures that data stored in 
the ConfigMap cannot be updated (only object metadata can be modified). If not 
set to true, the field can be modified at any time. Defaulted to nil.",
+      "type": "boolean"
+    },
+    "kind": {
+      "description": "Kind is a string value representing the REST resource 
this object represents. Servers may infer this from the endpoint the client 
submits requests to. Cannot be updated. In CamelCase. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds";,
+      "type": "string"
+    },
+    "metadata": {
+      "description": "Standard object's metadata. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata";,
+      "properties": {
+        "annotations": {
+          "additionalProperties": {
+            "type": "string"
+          },
+          "description": "Annotations is an unstructured key value map stored 
with a resource that may be set by external tools to store and retrieve 
arbitrary metadata. They are not queryable and should be preserved when 
modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations";,
+          "type": "object"
+        },
+        "clusterName": {
+          "description": "Deprecated: ClusterName is a legacy field that was 
always cleared by the system and never used; it will be removed completely in 
1.25.\n\nThe name in the go struct is changed to help clients detect accidental 
use.",
+          "type": "string"
+        },
+        "creationTimestamp": {
+          "description": "CreationTimestamp is a timestamp representing the 
server time when this object was created. It is not guaranteed to be set in 
happens-before order across separate operations. Clients may not set this 
value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the 
system. Read-only. Null for lists. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata";,
+          "format": "date-time",
+          "type": "string"
+        },
+        "deletionGracePeriodSeconds": {
+          "description": "Number of seconds allowed for this object to 
gracefully terminate before it will be removed from the system. Only set when 
deletionTimestamp is also set. May only be shortened. Read-only.",
+          "format": "int64",
+          "type": "integer"
+        },
+        "deletionTimestamp": {
+          "description": "DeletionTimestamp is RFC 3339 date and time at which 
this resource will be deleted. This field is set by the server when a graceful 
deletion is requested by the user, and is not directly settable by a client. 
The resource is expected to be deleted (no longer visible from resource lists, 
and not reachable by name) after the time in this field, once the finalizers 
list is empty. As long as the finalizers list contains items, deletion is 
blocked. Once the deletionT [...]
+          "format": "date-time",
+          "type": "string"
+        },
+        "finalizers": {
+          "description": "Must be empty before the object is deleted from the 
registry. Each entry is an identifier for the responsible component that will 
remove the entry from the list. If the deletionTimestamp of the object is 
non-nil, entries in this list can only be removed. Finalizers may be processed 
and removed in any order.  Order is NOT enforced because it introduces 
significant risk of stuck finalizers. finalizers is a shared field, any actor 
with permission can reorder it. If [...]
+          "items": {
+            "type": "string"
+          },
+          "type": "array"
+        },
+        "generateName": {
+          "description": "GenerateName is an optional prefix, used by the 
server, to generate a unique name ONLY IF the Name field has not been provided. 
If this field is used, the name returned to the client will be different than 
the name passed. This value will also be combined with a unique suffix. The 
provided value has the same validation rules as the Name field, and may be 
truncated by the length of the suffix required to make the value unique on the 
server.\n\nIf this field is sp [...]
+          "type": "string"
+        },
+        "generation": {
+          "description": "A sequence number representing a specific generation 
of the desired state. Populated by the system. Read-only.",
+          "format": "int64",
+          "type": "integer"
+        },
+        "labels": {
+          "additionalProperties": {
+            "type": "string"
+          },
+          "description": "Map of string keys and values that can be used to 
organize and categorize (scope and select) objects. May match selectors of 
replication controllers and services. More info: 
http://kubernetes.io/docs/user-guide/labels";,
+          "type": "object"
+        },
+        "managedFields": {
+          "description": "ManagedFields maps workflow-id and version to the 
set of fields that are managed by that workflow. This is mostly for internal 
housekeeping, and users typically shouldn't need to set or understand this 
field. A workflow can be the user's name, a controller's name, or the name of a 
specific apply path like \"ci-cd\". The set of fields is always in the version 
that the workflow used when modifying the object.",
+          "items": {
+            "description": "ManagedFieldsEntry is a workflow-id, a FieldSet 
and the group version of the resource that the fieldset applies to.",
+            "properties": {
+              "apiVersion": {
+                "description": "APIVersion defines the version of this 
resource that this field set applies to. The format is \"group/version\" just 
like the top-level APIVersion field. It is necessary to track the version of a 
field set because it cannot be automatically converted.",
+                "type": "string"
+              },
+              "fieldsType": {
+                "description": "FieldsType is the discriminator for the 
different fields format and version. There is currently only one possible 
value: \"FieldsV1\"",
+                "type": "string"
+              },
+              "fieldsV1": {
+                "description": "FieldsV1 holds the first JSON version format 
as described in the \"FieldsV1\" type.",
+                "type": "object"
+              },
+              "manager": {
+                "description": "Manager is an identifier of the workflow 
managing these fields.",
+                "type": "string"
+              },
+              "operation": {
+                "description": "Operation is the type of operation which lead 
to this ManagedFieldsEntry being created. The only valid values for this field 
are 'Apply' and 'Update'.",
+                "type": "string"
+              },
+              "subresource": {
+                "description": "Subresource is the name of the subresource 
used to update that object, or empty string if the object was updated through 
the main resource. The value of this field is used to distinguish between 
managers, even if they share the same name. For example, a status update will 
be distinct from a regular update using the same manager name. Note that the 
APIVersion field is not related to the Subresource field and it always 
corresponds to the version of the main  [...]
+                "type": "string"
+              },
+              "time": {
+                "description": "Time is the timestamp of when the 
ManagedFields entry was added. The timestamp will also be updated if a field is 
added, the manager changes any of the owned fields value or removes a field. 
The timestamp does not update when a field is removed from the entry because 
another manager took it over.",
+                "format": "date-time",
+                "type": "string"
+              }
+            },
+            "type": "object"
+          },
+          "type": "array"
+        },
+        "name": {
+          "description": "Name must be unique within a namespace. Is required 
when creating resources, although some resources may allow a client to request 
the generation of an appropriate name automatically. Name is primarily intended 
for creation idempotence and configuration definition. Cannot be updated. More 
info: http://kubernetes.io/docs/user-guide/identifiers#names";,
+          "type": "string"
+        },
+        "namespace": {
+          "description": "Namespace defines the space within which each name 
must be unique. An empty namespace is equivalent to the \"default\" namespace, 
but \"default\" is the canonical representation. Not all objects are required 
to be scoped to a namespace - the value of this field for those objects will be 
empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: 
http://kubernetes.io/docs/user-guide/namespaces";,
+          "type": "string"
+        },
+        "ownerReferences": {
+          "description": "List of objects depended by this object. If ALL 
objects in the list have been deleted, this object will be garbage collected. 
If this object is managed by a controller, then an entry in this list will 
point to this controller, with the controller field set to true. There cannot 
be more than one managing controller.",
+          "items": {
+            "description": "OwnerReference contains enough information to let 
you identify an owning object. An owning object must be in the same namespace 
as the dependent, or be cluster-scoped, so there is no namespace field.",
+            "properties": {
+              "apiVersion": {
+                "description": "API version of the referent.",
+                "type": "string"
+              },
+              "blockOwnerDeletion": {
+                "description": "If true, AND if the owner has the 
\"foregroundDeletion\" finalizer, then the owner cannot be deleted from the 
key-value store until this reference is removed. See 
https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion
 for how the garbage collector interacts with this field and enforces the 
foreground deletion. Defaults to false. To set this field, a user needs 
\"delete\" permission of the owner, otherwise 422 (Unprocessabl [...]
+                "type": "boolean"
+              },
+              "controller": {
+                "description": "If true, this reference points to the managing 
controller.",
+                "type": "boolean"
+              },
+              "kind": {
+                "description": "Kind of the referent. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds";,
+                "type": "string"
+              },
+              "name": {
+                "description": "Name of the referent. More info: 
http://kubernetes.io/docs/user-guide/identifiers#names";,
+                "type": "string"
+              },
+              "uid": {
+                "description": "UID of the referent. More info: 
http://kubernetes.io/docs/user-guide/identifiers#uids";,
+                "type": "string"
+              }
+            },
+            "required": [
+              "apiVersion",
+              "kind",
+              "name",
+              "uid"
+            ],
+            "type": "object"
+          },
+          "type": "array"
+        },
+        "resourceVersion": {
+          "description": "An opaque value that represents the internal version 
of this object that can be used by clients to determine when objects have 
changed. May be used for optimistic concurrency, change detection, and the 
watch operation on a resource or set of resources. Clients must treat these 
values as opaque and passed unmodified back to the server. They may only be 
valid for a particular resource or set of resources.\n\nPopulated by the 
system. Read-only. Value must be treate [...]
+          "type": "string"
+        },
+        "selfLink": {
+          "description": "Deprecated: selfLink is a legacy read-only field 
that is no longer populated by the system.",
+          "type": "string"
+        },
+        "uid": {
+          "description": "UID is the unique in time and space value for this 
object. It is typically generated by the server on successful creation of a 
resource and is not allowed to change on PUT operations.\n\nPopulated by the 
system. Read-only. More info: 
http://kubernetes.io/docs/user-guide/identifiers#uids";,
+          "type": "string"
+        }
+      },
+      "type": "object"
+    }
+  },
+  "type": "object",
+  "x-kubernetes-group-version-kind": [
+    {
+      "group": "",
+      "kind": "ConfigMap",
+      "version": "v1"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/johnzon-jsonschema/src/test/resources/Node.json 
b/johnzon-jsonschema/src/test/resources/Node.json
new file mode 100644
index 00000000..ddc5cc39
--- /dev/null
+++ b/johnzon-jsonschema/src/test/resources/Node.json
@@ -0,0 +1,634 @@
+{
+  "description":"Node is a worker node in Kubernetes. Each node will have a 
unique identifier in the cache (i.e. in etcd).",
+  "properties":{
+    "apiVersion":{
+      "description":"APIVersion defines the versioned schema of this 
representation of an object. Servers should convert recognized schemas to the 
latest internal value, and may reject unrecognized values. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources";,
+      "type":"string"
+    },
+    "kind":{
+      "description":"Kind is a string value representing the REST resource 
this object represents. Servers may infer this from the endpoint the client 
submits requests to. Cannot be updated. In CamelCase. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds";,
+      "type":"string"
+    },
+    "metadata":{
+      "description":"Standard object's metadata. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata";,
+      "properties":{
+        "annotations":{
+          "additionalProperties":{
+            "type":"string"
+          },
+          "description":"Annotations is an unstructured key value map stored 
with a resource that may be set by external tools to store and retrieve 
arbitrary metadata. They are not queryable and should be preserved when 
modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations";,
+          "type":"object"
+        },
+        "clusterName":{
+          "description":"The name of the cluster which the object belongs to. 
This is used to distinguish resources with same name and namespace in different 
clusters. This field is not set anywhere right now and apiserver is going to 
ignore it if set in create or update request.",
+          "type":"string"
+        },
+        "creationTimestamp":{
+          "description":"CreationTimestamp is a timestamp representing the 
server time when this object was created. It is not guaranteed to be set in 
happens-before order across separate operations. Clients may not set this 
value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the 
system. Read-only. Null for lists. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata";,
+          "format":"date-time",
+          "type":"string"
+        },
+        "deletionGracePeriodSeconds":{
+          "description":"Number of seconds allowed for this object to 
gracefully terminate before it will be removed from the system. Only set when 
deletionTimestamp is also set. May only be shortened. Read-only.",
+          "format":"int64",
+          "type":"integer"
+        },
+        "deletionTimestamp":{
+          "description":"DeletionTimestamp is RFC 3339 date and time at which 
this resource will be deleted. This field is set by the server when a graceful 
deletion is requested by the user, and is not directly settable by a client. 
The resource is expected to be deleted (no longer visible from resource lists, 
and not reachable by name) after the time in this field, once the finalizers 
list is empty. As long as the finalizers list contains items, deletion is 
blocked. Once the deletionTi [...]
+          "format":"date-time",
+          "type":"string"
+        },
+        "finalizers":{
+          "description":"Must be empty before the object is deleted from the 
registry. Each entry is an identifier for the responsible component that will 
remove the entry from the list. If the deletionTimestamp of the object is 
non-nil, entries in this list can only be removed. Finalizers may be processed 
and removed in any order.  Order is NOT enforced because it introduces 
significant risk of stuck finalizers. finalizers is a shared field, any actor 
with permission can reorder it. If  [...]
+          "items":{
+            "type":"string"
+          },
+          "type":"array"
+        },
+        "generateName":{
+          "description":"GenerateName is an optional prefix, used by the 
server, to generate a unique name ONLY IF the Name field has not been provided. 
If this field is used, the name returned to the client will be different than 
the name passed. This value will also be combined with a unique suffix. The 
provided value has the same validation rules as the Name field, and may be 
truncated by the length of the suffix required to make the value unique on the 
server.\n\nIf this field is spe [...]
+          "type":"string"
+        },
+        "generation":{
+          "description":"A sequence number representing a specific generation 
of the desired state. Populated by the system. Read-only.",
+          "format":"int64",
+          "type":"integer"
+        },
+        "labels":{
+          "additionalProperties":{
+            "type":"string"
+          },
+          "description":"Map of string keys and values that can be used to 
organize and categorize (scope and select) objects. May match selectors of 
replication controllers and services. More info: 
http://kubernetes.io/docs/user-guide/labels";,
+          "type":"object"
+        },
+        "managedFields":{
+          "description":"ManagedFields maps workflow-id and version to the set 
of fields that are managed by that workflow. This is mostly for internal 
housekeeping, and users typically shouldn't need to set or understand this 
field. A workflow can be the user's name, a controller's name, or the name of a 
specific apply path like \"ci-cd\". The set of fields is always in the version 
that the workflow used when modifying the object.",
+          "items":{
+            "description":"ManagedFieldsEntry is a workflow-id, a FieldSet and 
the group version of the resource that the fieldset applies to.",
+            "properties":{
+              "apiVersion":{
+                "description":"APIVersion defines the version of this resource 
that this field set applies to. The format is \"group/version\" just like the 
top-level APIVersion field. It is necessary to track the version of a field set 
because it cannot be automatically converted.",
+                "type":"string"
+              },
+              "fieldsType":{
+                "description":"FieldsType is the discriminator for the 
different fields format and version. There is currently only one possible 
value: \"FieldsV1\"",
+                "type":"string"
+              },
+              "fieldsV1":{
+                "description":"FieldsV1 holds the first JSON version format as 
described in the \"FieldsV1\" type.",
+                "type":"object"
+              },
+              "manager":{
+                "description":"Manager is an identifier of the workflow 
managing these fields.",
+                "type":"string"
+              },
+              "operation":{
+                "description":"Operation is the type of operation which lead 
to this ManagedFieldsEntry being created. The only valid values for this field 
are 'Apply' and 'Update'.",
+                "type":"string"
+              },
+              "subresource":{
+                "description":"Subresource is the name of the subresource used 
to update that object, or empty string if the object was updated through the 
main resource. The value of this field is used to distinguish between managers, 
even if they share the same name. For example, a status update will be distinct 
from a regular update using the same manager name. Note that the APIVersion 
field is not related to the Subresource field and it always corresponds to the 
version of the main r [...]
+                "type":"string"
+              },
+              "time":{
+                "description":"Time is timestamp of when these fields were 
set. It should always be empty if Operation is 'Apply'",
+                "format":"date-time",
+                "type":"string"
+              }
+            },
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "name":{
+          "description":"Name must be unique within a namespace. Is required 
when creating resources, although some resources may allow a client to request 
the generation of an appropriate name automatically. Name is primarily intended 
for creation idempotence and configuration definition. Cannot be updated. More 
info: http://kubernetes.io/docs/user-guide/identifiers#names";,
+          "type":"string"
+        },
+        "namespace":{
+          "description":"Namespace defines the space within which each name 
must be unique. An empty namespace is equivalent to the \"default\" namespace, 
but \"default\" is the canonical representation. Not all objects are required 
to be scoped to a namespace - the value of this field for those objects will be 
empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: 
http://kubernetes.io/docs/user-guide/namespaces";,
+          "type":"string"
+        },
+        "ownerReferences":{
+          "description":"List of objects depended by this object. If ALL 
objects in the list have been deleted, this object will be garbage collected. 
If this object is managed by a controller, then an entry in this list will 
point to this controller, with the controller field set to true. There cannot 
be more than one managing controller.",
+          "items":{
+            "description":"OwnerReference contains enough information to let 
you identify an owning object. An owning object must be in the same namespace 
as the dependent, or be cluster-scoped, so there is no namespace field.",
+            "properties":{
+              "apiVersion":{
+                "description":"API version of the referent.",
+                "type":"string"
+              },
+              "blockOwnerDeletion":{
+                "description":"If true, AND if the owner has the 
\"foregroundDeletion\" finalizer, then the owner cannot be deleted from the 
key-value store until this reference is removed. Defaults to false. To set this 
field, a user needs \"delete\" permission of the owner, otherwise 422 
(Unprocessable Entity) will be returned.",
+                "type":"boolean"
+              },
+              "controller":{
+                "description":"If true, this reference points to the managing 
controller.",
+                "type":"boolean"
+              },
+              "kind":{
+                "description":"Kind of the referent. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds";,
+                "type":"string"
+              },
+              "name":{
+                "description":"Name of the referent. More info: 
http://kubernetes.io/docs/user-guide/identifiers#names";,
+                "type":"string"
+              },
+              "uid":{
+                "description":"UID of the referent. More info: 
http://kubernetes.io/docs/user-guide/identifiers#uids";,
+                "type":"string"
+              }
+            },
+            "required":[
+              "apiVersion",
+              "kind",
+              "name",
+              "uid"
+            ],
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "resourceVersion":{
+          "description":"An opaque value that represents the internal version 
of this object that can be used by clients to determine when objects have 
changed. May be used for optimistic concurrency, change detection, and the 
watch operation on a resource or set of resources. Clients must treat these 
values as opaque and passed unmodified back to the server. They may only be 
valid for a particular resource or set of resources.\n\nPopulated by the 
system. Read-only. Value must be treated [...]
+          "type":"string"
+        },
+        "selfLink":{
+          "description":"SelfLink is a URL representing this object. Populated 
by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this 
field in 1.20 release and the field is planned to be removed in 1.21 release.",
+          "type":"string"
+        },
+        "uid":{
+          "description":"UID is the unique in time and space value for this 
object. It is typically generated by the server on successful creation of a 
resource and is not allowed to change on PUT operations.\n\nPopulated by the 
system. Read-only. More info: 
http://kubernetes.io/docs/user-guide/identifiers#uids";,
+          "type":"string"
+        }
+      },
+      "type":"object"
+    },
+    "spec":{
+      "description":"Spec defines the behavior of a node. 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status";,
+      "properties":{
+        "configSource":{
+          "description":"Deprecated. If specified, the source of the node's 
configuration. The DynamicKubeletConfig feature gate must be enabled for the 
Kubelet to use this field. This field is deprecated as of 1.22: 
https://git.k8s.io/enhancements/keps/sig-node/281-dynamic-kubelet-configuration";,
+          "properties":{
+            "configMap":{
+              "description":"ConfigMap is a reference to a Node's ConfigMap",
+              "properties":{
+                "kubeletConfigKey":{
+                  "description":"KubeletConfigKey declares which key of the 
referenced ConfigMap corresponds to the KubeletConfiguration structure This 
field is required in all cases.",
+                  "type":"string"
+                },
+                "name":{
+                  "description":"Name is the metadata.name of the referenced 
ConfigMap. This field is required in all cases.",
+                  "type":"string"
+                },
+                "namespace":{
+                  "description":"Namespace is the metadata.namespace of the 
referenced ConfigMap. This field is required in all cases.",
+                  "type":"string"
+                },
+                "resourceVersion":{
+                  "description":"ResourceVersion is the 
metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden 
in Node.Spec, and required in Node.Status.",
+                  "type":"string"
+                },
+                "uid":{
+                  "description":"UID is the metadata.UID of the referenced 
ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
+                  "type":"string"
+                }
+              },
+              "required":[
+                "namespace",
+                "name",
+                "kubeletConfigKey"
+              ],
+              "type":"object"
+            }
+          },
+          "type":"object"
+        },
+        "externalID":{
+          "description":"Deprecated. Not all kubelets will set this field. 
Remove field after 1.13. see: https://issues.k8s.io/61966";,
+          "type":"string"
+        },
+        "podCIDR":{
+          "description":"PodCIDR represents the pod IP range assigned to the 
node.",
+          "type":"string"
+        },
+        "podCIDRs":{
+          "description":"podCIDRs represents the IP ranges assigned to the 
node for usage by Pods on that node. If this field is specified, the 0th entry 
must match the podCIDR field. It may contain at most 1 value for each of IPv4 
and IPv6.",
+          "items":{
+            "type":"string"
+          },
+          "type":"array"
+        },
+        "providerID":{
+          "description":"ID of the node assigned by the cloud provider in the 
format: <ProviderName>://<ProviderSpecificNodeID>",
+          "type":"string"
+        },
+        "taints":{
+          "description":"If specified, the node's taints.",
+          "items":{
+            "description":"The node this Taint is attached to has the 
\"effect\" on any pod that does not tolerate the Taint.",
+            "properties":{
+              "effect":{
+                "description":"Required. The effect of the taint on pods that 
do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule and 
NoExecute.\n\nPossible enum values:\n - `\"NoExecute\"` Evict any 
already-running pods that do not tolerate the taint. Currently enforced by 
NodeController.\n - `\"NoSchedule\"` Do not allow new pods to schedule onto the 
node unless they tolerate the taint, but allow all pods submitted to Kubelet 
without going through the scheduler to [...]
+                "enum":[
+                  "NoExecute",
+                  "NoSchedule",
+                  "PreferNoSchedule"
+                ],
+                "type":"string"
+              },
+              "key":{
+                "description":"Required. The taint key to be applied to a 
node.",
+                "type":"string"
+              },
+              "timeAdded":{
+                "description":"TimeAdded represents the time at which the 
taint was added. It is only written for NoExecute taints.",
+                "format":"date-time",
+                "type":"string"
+              },
+              "value":{
+                "description":"The taint value corresponding to the taint 
key.",
+                "type":"string"
+              }
+            },
+            "required":[
+              "key",
+              "effect"
+            ],
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "unschedulable":{
+          "description":"Unschedulable controls node schedulability of new 
pods. By default, node is schedulable. More info: 
https://kubernetes.io/docs/concepts/nodes/node/#manual-node-administration";,
+          "type":"boolean"
+        }
+      },
+      "type":"object"
+    },
+    "status":{
+      "description":"Most recently observed status of the node. Populated by 
the system. Read-only. More info: 
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status";,
+      "properties":{
+        "addresses":{
+          "description":"List of addresses reachable to the node. Queried from 
cloud provider, if available. More info: 
https://kubernetes.io/docs/concepts/nodes/node/#addresses Note: This field is 
declared as mergeable, but the merge key is not sufficiently unique, which can 
cause data corruption when it is merged. Callers should instead use a 
full-replacement patch. See http://pr.k8s.io/79391 for an example.",
+          "items":{
+            "description":"NodeAddress contains information for the node's 
address.",
+            "properties":{
+              "address":{
+                "description":"The node address.",
+                "type":"string"
+              },
+              "type":{
+                "description":"Node address type, one of Hostname, ExternalIP 
or InternalIP.\n\nPossible enum values:\n - `\"ExternalDNS\"` identifies a DNS 
name which resolves to an IP address which has the characteristics of a 
NodeExternalIP. The IP it resolves to may or may not be a listed NodeExternalIP 
address.\n - `\"ExternalIP\"` identifies an IP address which is, in some way, 
intended to be more usable from outside the cluster then an internal IP, though 
no specific semantics are [...]
+                "enum":[
+                  "ExternalDNS",
+                  "ExternalIP",
+                  "Hostname",
+                  "InternalDNS",
+                  "InternalIP"
+                ],
+                "type":"string"
+              }
+            },
+            "required":[
+              "type",
+              "address"
+            ],
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "allocatable":{
+          "additionalProperties":{
+            "description":"Quantity is a fixed-point representation of a 
number. It provides convenient marshaling/unmarshaling in JSON and YAML, in 
addition to String() and AsInt64() accessors.\n\nThe serialization format 
is:\n\n<quantity>        ::= <signedNumber><suffix>\n  (Note that <suffix> may 
be empty, from the \"\" case in <decimalSI>.)\n<digit>           ::= 0 | 1 | 
... | 9 <digits>          ::= <digit> | <digit><digits> <number>          ::= 
<digits> | <digits>.<digits> | <dig [...]
+            "type":"string"
+          },
+          "description":"Allocatable represents the resources of a node that 
are available for scheduling. Defaults to Capacity.",
+          "type":"object"
+        },
+        "capacity":{
+          "additionalProperties":{
+            "description":"Quantity is a fixed-point representation of a 
number. It provides convenient marshaling/unmarshaling in JSON and YAML, in 
addition to String() and AsInt64() accessors.\n\nThe serialization format 
is:\n\n<quantity>        ::= <signedNumber><suffix>\n  (Note that <suffix> may 
be empty, from the \"\" case in <decimalSI>.)\n<digit>           ::= 0 | 1 | 
... | 9 <digits>          ::= <digit> | <digit><digits> <number>          ::= 
<digits> | <digits>.<digits> | <dig [...]
+            "type":"string"
+          },
+          "description":"Capacity represents the total resources of a node. 
More info: 
https://kubernetes.io/docs/concepts/storage/persistent-volumes#capacity";,
+          "type":"object"
+        },
+        "conditions":{
+          "description":"Conditions is an array of current observed node 
conditions. More info: 
https://kubernetes.io/docs/concepts/nodes/node/#condition";,
+          "items":{
+            "description":"NodeCondition contains condition information for a 
node.",
+            "properties":{
+              "lastHeartbeatTime":{
+                "description":"Last time we got an update on a given 
condition.",
+                "format":"date-time",
+                "type":"string"
+              },
+              "lastTransitionTime":{
+                "description":"Last time the condition transit from one status 
to another.",
+                "format":"date-time",
+                "type":"string"
+              },
+              "message":{
+                "description":"Human readable message indicating details about 
last transition.",
+                "type":"string"
+              },
+              "reason":{
+                "description":"(brief) reason for the condition's last 
transition.",
+                "type":"string"
+              },
+              "status":{
+                "description":"Status of the condition, one of True, False, 
Unknown.",
+                "type":"string"
+              },
+              "type":{
+                "description":"Type of node condition.\n\nPossible enum 
values:\n - `\"DiskPressure\"` means the kubelet is under pressure due to 
insufficient available disk.\n - `\"MemoryPressure\"` means the kubelet is 
under pressure due to insufficient available memory.\n - 
`\"NetworkUnavailable\"` means that network for the node is not correctly 
configured.\n - `\"PIDPressure\"` means the kubelet is under pressure due to 
insufficient available PID.\n - `\"Ready\"` means kubelet is he [...]
+                "enum":[
+                  "DiskPressure",
+                  "MemoryPressure",
+                  "NetworkUnavailable",
+                  "PIDPressure",
+                  "Ready"
+                ],
+                "type":"string"
+              }
+            },
+            "required":[
+              "type",
+              "status"
+            ],
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "config":{
+          "description":"Status of the config assigned to the node via the 
dynamic Kubelet config feature.",
+          "properties":{
+            "active":{
+              "description":"Active reports the checkpointed config the node 
is actively using. Active will represent either the current version of the 
Assigned config, or the current LastKnownGood config, depending on whether 
attempting to use the Assigned config results in an error.",
+              "properties":{
+                "configMap":{
+                  "description":"ConfigMap is a reference to a Node's 
ConfigMap",
+                  "properties":{
+                    "kubeletConfigKey":{
+                      "description":"KubeletConfigKey declares which key of 
the referenced ConfigMap corresponds to the KubeletConfiguration structure This 
field is required in all cases.",
+                      "type":"string"
+                    },
+                    "name":{
+                      "description":"Name is the metadata.name of the 
referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "namespace":{
+                      "description":"Namespace is the metadata.namespace of 
the referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "resourceVersion":{
+                      "description":"ResourceVersion is the 
metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden 
in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    },
+                    "uid":{
+                      "description":"UID is the metadata.UID of the referenced 
ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    }
+                  },
+                  "required":[
+                    "namespace",
+                    "name",
+                    "kubeletConfigKey"
+                  ],
+                  "type":"object"
+                }
+              },
+              "type":"object"
+            },
+            "assigned":{
+              "description":"Assigned reports the checkpointed config the node 
will try to use. When Node.Spec.ConfigSource is updated, the node checkpoints 
the associated config payload to local disk, along with a record indicating 
intended config. The node refers to this record to choose its config 
checkpoint, and reports this record in Assigned. Assigned only updates in the 
status after the record has been checkpointed to disk. When the Kubelet is 
restarted, it tries to make the Assig [...]
+              "properties":{
+                "configMap":{
+                  "description":"ConfigMap is a reference to a Node's 
ConfigMap",
+                  "properties":{
+                    "kubeletConfigKey":{
+                      "description":"KubeletConfigKey declares which key of 
the referenced ConfigMap corresponds to the KubeletConfiguration structure This 
field is required in all cases.",
+                      "type":"string"
+                    },
+                    "name":{
+                      "description":"Name is the metadata.name of the 
referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "namespace":{
+                      "description":"Namespace is the metadata.namespace of 
the referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "resourceVersion":{
+                      "description":"ResourceVersion is the 
metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden 
in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    },
+                    "uid":{
+                      "description":"UID is the metadata.UID of the referenced 
ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    }
+                  },
+                  "required":[
+                    "namespace",
+                    "name",
+                    "kubeletConfigKey"
+                  ],
+                  "type":"object"
+                }
+              },
+              "type":"object"
+            },
+            "error":{
+              "description":"Error describes any problems reconciling the 
Spec.ConfigSource to the Active config. Errors may occur, for example, 
attempting to checkpoint Spec.ConfigSource to the local Assigned record, 
attempting to checkpoint the payload associated with Spec.ConfigSource, 
attempting to load or validate the Assigned config, etc. Errors may occur at 
different points while syncing config. Earlier errors (e.g. download or 
checkpointing errors) will not result in a rollback t [...]
+              "type":"string"
+            },
+            "lastKnownGood":{
+              "description":"LastKnownGood reports the checkpointed config the 
node will fall back to when it encounters an error attempting to use the 
Assigned config. The Assigned config becomes the LastKnownGood config when the 
node determines that the Assigned config is stable and correct. This is 
currently implemented as a 10-minute soak period starting when the local record 
of Assigned config is updated. If the Assigned config is Active at the end of 
this period, it becomes the Las [...]
+              "properties":{
+                "configMap":{
+                  "description":"ConfigMap is a reference to a Node's 
ConfigMap",
+                  "properties":{
+                    "kubeletConfigKey":{
+                      "description":"KubeletConfigKey declares which key of 
the referenced ConfigMap corresponds to the KubeletConfiguration structure This 
field is required in all cases.",
+                      "type":"string"
+                    },
+                    "name":{
+                      "description":"Name is the metadata.name of the 
referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "namespace":{
+                      "description":"Namespace is the metadata.namespace of 
the referenced ConfigMap. This field is required in all cases.",
+                      "type":"string"
+                    },
+                    "resourceVersion":{
+                      "description":"ResourceVersion is the 
metadata.ResourceVersion of the referenced ConfigMap. This field is forbidden 
in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    },
+                    "uid":{
+                      "description":"UID is the metadata.UID of the referenced 
ConfigMap. This field is forbidden in Node.Spec, and required in Node.Status.",
+                      "type":"string"
+                    }
+                  },
+                  "required":[
+                    "namespace",
+                    "name",
+                    "kubeletConfigKey"
+                  ],
+                  "type":"object"
+                }
+              },
+              "type":"object"
+            }
+          },
+          "type":"object"
+        },
+        "daemonEndpoints":{
+          "description":"Endpoints of daemons running on the Node.",
+          "properties":{
+            "kubeletEndpoint":{
+              "description":"Endpoint on which Kubelet is listening.",
+              "properties":{
+                "Port":{
+                  "description":"Port number of the given endpoint.",
+                  "format":"int32",
+                  "type":"integer"
+                }
+              },
+              "required":[
+                "Port"
+              ],
+              "type":"object"
+            }
+          },
+          "type":"object"
+        },
+        "images":{
+          "description":"List of container images on this node",
+          "items":{
+            "description":"Describe a container image",
+            "properties":{
+              "names":{
+                "description":"Names by which this image is known. e.g. 
[\"k8s.gcr.io/hyperkube:v1.0.7\", 
\"dockerhub.io/google_containers/hyperkube:v1.0.7\"]",
+                "items":{
+                  "type":"string"
+                },
+                "type":"array"
+              },
+              "sizeBytes":{
+                "description":"The size of the image in bytes.",
+                "format":"int64",
+                "type":"integer"
+              }
+            },
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "nodeInfo":{
+          "description":"Set of ids/uuids to uniquely identify the node. More 
info: https://kubernetes.io/docs/concepts/nodes/node/#info";,
+          "properties":{
+            "architecture":{
+              "description":"The Architecture reported by the node",
+              "type":"string"
+            },
+            "bootID":{
+              "description":"Boot ID reported by the node.",
+              "type":"string"
+            },
+            "containerRuntimeVersion":{
+              "description":"ContainerRuntime Version reported by the node 
through runtime remote API (e.g. docker://1.5.0).",
+              "type":"string"
+            },
+            "kernelVersion":{
+              "description":"Kernel Version reported by the node from 'uname 
-r' (e.g. 3.16.0-0.bpo.4-amd64).",
+              "type":"string"
+            },
+            "kubeProxyVersion":{
+              "description":"KubeProxy Version reported by the node.",
+              "type":"string"
+            },
+            "kubeletVersion":{
+              "description":"Kubelet Version reported by the node.",
+              "type":"string"
+            },
+            "machineID":{
+              "description":"MachineID reported by the node. For unique 
machine identification in the cluster this field is preferred. Learn more from 
man(5) machine-id: http://man7.org/linux/man-pages/man5/machine-id.5.html";,
+              "type":"string"
+            },
+            "operatingSystem":{
+              "description":"The Operating System reported by the node",
+              "type":"string"
+            },
+            "osImage":{
+              "description":"OS Image reported by the node from 
/etc/os-release (e.g. Debian GNU/Linux 7 (wheezy)).",
+              "type":"string"
+            },
+            "systemUUID":{
+              "description":"SystemUUID reported by the node. For unique 
machine identification MachineID is preferred. This field is specific to Red 
Hat hosts 
https://access.redhat.com/documentation/en-us/red_hat_subscription_management/1/html/rhsm/uuid";,
+              "type":"string"
+            }
+          },
+          "required":[
+            "machineID",
+            "systemUUID",
+            "bootID",
+            "kernelVersion",
+            "osImage",
+            "containerRuntimeVersion",
+            "kubeletVersion",
+            "kubeProxyVersion",
+            "operatingSystem",
+            "architecture"
+          ],
+          "type":"object"
+        },
+        "phase":{
+          "description":"NodePhase is the recently observed lifecycle phase of 
the node. More info: https://kubernetes.io/docs/concepts/nodes/node/#phase The 
field is never populated, and now is deprecated.\n\nPossible enum values:\n - 
`\"Pending\"` means the node has been created/added by the system, but not 
configured.\n - `\"Running\"` means the node has been configured and has 
Kubernetes components running.\n - `\"Terminated\"` means the node has been 
removed from the cluster.",
+          "enum":[
+            "Pending",
+            "Running",
+            "Terminated"
+          ],
+          "type":"string"
+        },
+        "volumesAttached":{
+          "description":"List of volumes that are attached to the node.",
+          "items":{
+            "description":"AttachedVolume describes a volume attached to a 
node",
+            "properties":{
+              "devicePath":{
+                "description":"DevicePath represents the device path where the 
volume should be available",
+                "type":"string"
+              },
+              "name":{
+                "description":"Name of the attached volume",
+                "type":"string"
+              }
+            },
+            "required":[
+              "name",
+              "devicePath"
+            ],
+            "type":"object"
+          },
+          "type":"array"
+        },
+        "volumesInUse":{
+          "description":"List of attachable volumes in use (mounted) by the 
node.",
+          "items":{
+            "type":"string"
+          },
+          "type":"array"
+        }
+      },
+      "type":"object"
+    }
+  },
+  "type":"object",
+  "x-kubernetes-group-version-kind":[
+    {
+      "group":"",
+      "kind":"Node",
+      "version":"v1"
+    }
+  ]
+}
\ No newline at end of file
diff --git 
a/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/JsonSchemaToPojoMojo.java
 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/JsonSchemaToPojoMojo.java
new file mode 100644
index 00000000..074f7f18
--- /dev/null
+++ 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/JsonSchemaToPojoMojo.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.maven.plugin;
+
+import org.apache.johnzon.jsonschema.generator.PojoGenerator;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonReaderFactory;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+import static 
org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
+
+/**
+ * Generates mojo bindings from json schema specification(s).
+ */
+@Mojo(name = "jsonschema2pojo", defaultPhase = GENERATE_SOURCES)
+public class JsonSchemaToPojoMojo extends AbstractMojo {
+    /**
+     * Generation configuration.
+     * Note that if source is a directory, class name is ignored and auto set 
from schema name.
+     */
+    @Parameter
+    private PojoGenerator.PojoConfiguration generator;
+
+    /**
+     * Extensions to consider if source is a directory.
+     */
+    @Parameter(property = "johnzon.jsonschema.extensions", defaultValue = 
".jsonschema.json")
+    private List<String> jsonSchemaExtensions;
+
+    /**
+     * Source jsonschema or directory containing json schemas.
+     */
+    @Parameter(property = "johnzon.source", defaultValue = 
"${project.basedir}/src/main/johnzon/jsonschema")
+    private File source;
+
+    /**
+     * Where to dump generated classes.
+     */
+    @Parameter(property = "johnzon.target", defaultValue = 
"${project.build.directory}/generated-sources/johnzon-pojo")
+    private File target;
+
+    @Override
+    public void execute() {
+        final JsonReaderFactory readerFactory = 
Json.createReaderFactory(emptyMap());
+        if (source.isDirectory()) {
+            try {
+                Files.walkFileTree(source.toPath(), new 
SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file, 
BasicFileAttributes attrs) throws IOException {
+                        final String name = file.getFileName().toString();
+                        final List<String> matchingExt = 
jsonSchemaExtensions.stream()
+                                .filter(name::endsWith)
+                                .sorted(comparing(String::length).reversed())
+                                .collect(toList());
+                        if (matchingExt.size() >= 1) {
+                            final PojoGenerator.PojoConfiguration conf = 
generator == null ? new PojoGenerator.PojoConfiguration() : generator;
+                            conf.setClassName(name.substring(0, name.length() 
- matchingExt.get(0).length()));
+                            dump(new PojoGenerator(conf)
+                                    .visitSchema(read(readerFactory, 
source.toPath()))
+                                    .generate());
+                        }
+                        return super.visitFile(file, attrs);
+                    }
+                });
+            } catch (final IOException e) {
+                throw new IllegalStateException(e);
+            }
+        } else {
+            dump(new PojoGenerator(generator == null ? new 
PojoGenerator.PojoConfiguration() : generator)
+                    .visitSchema(read(readerFactory, source.toPath()))
+                    .generate());
+        }
+    }
+
+    private JsonObject read(final JsonReaderFactory readerFactory, final Path 
path) {
+        try (final JsonReader reader = 
readerFactory.createReader(Files.newBufferedReader(path))) {
+            return reader.readObject();
+        } catch (final IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private void dump(final Map<String, String> generate) {
+        final Path root = target.toPath();
+        try {
+            for (final Map.Entry<String, String> entry : generate.entrySet()) {
+                final Path out = root.resolve(entry.getKey());
+                Files.createDirectories(out.getParent());
+                Files.write(out, 
entry.getValue().getBytes(StandardCharsets.UTF_8));
+            }
+        } catch (final IOException ioe) {
+            throw new IllegalStateException(ioe);
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index 19e9f038..c5ef5793 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,8 +36,8 @@
   <url>http://johnzon.apache.org</url>
 
   <properties>
-    <geronimo-jsonp.version>1.3</geronimo-jsonp.version>
-    <geronimo-jsonb.version>1.2</geronimo-jsonb.version>
+    <geronimo-jsonp.version>1.5</geronimo-jsonp.version>
+    <geronimo-jsonb.version>1.4</geronimo-jsonb.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     
<johnzon.site.url>https://svn.apache.org/repos/asf/johnzon/site/publish/</johnzon.site.url>
     <pubsub.url>scm:svn:${johnzon.site.url}</pubsub.url>

Reply via email to