16

I want to generate JSON schema where "additionalProperties" : false will be applied for all classes which I have.

Suppose I have following classes:

class A{
    private String s;
    private B b;

    public String getS() {
        return s;
    }

    public B getB() {
        return b;
    }
}

class B{
    private BigDecimal bd;

    public BigDecimal getBd() {
        return bd;
    }
}

When I am generating schema as following like below code the schema property "additionalProperties" : false was applying only for the class A.

ObjectMapper mapper = new ObjectMapper();
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
ObjectSchema schema = schemaGen.generateSchema(A.class).asObjectSchema();
schema.rejectAdditionalProperties();
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema);

How can I generate the schema where "additionalProperties" : false will be applied on all classes?

Example of schema

{ "type" : "object", "id" : "urn:jsonschema:com.xxx.xxx:A", "additionalProperties" : false, "properties" : { "s" : { "type" : "string" }, "b" : { "type" : "object", "id" : "urn:jsonschema:com.xxx.xxx:B", "properties" : { "bd" : { "type" : "number" } } } } }

Note: I don't want to generate schemes part by part.

For info: I have opened issue for this scenario if someone interested you can support fix of this issue. Generate json schema which should rejects all additional content

Beno
  • 945
  • 11
  • 22

4 Answers4

4

You will need to specify the schema for each properties like:

ObjectMapper mapper = new ObjectMapper();
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
ObjectSchema schemaB = schemaGen.generateSchema(B.class).asObjectSchema();
schemaB.rejectAdditionalProperties();

ObjectSchema schema = schemaGen.generateSchema(A.class).asObjectSchema();
schema.rejectAdditionalProperties();
schema.putProperty("b", schemaB);

You can leverage reflection api to automatically do it for you. Here is a quick and dirty example:

public static void main(String[] args) throws JsonProcessingException {
    final ObjectMapper mapper = new ObjectMapper();
    final JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
    ObjectSchema schema = generateSchema(schemaGen, A.class);
    schema.rejectAdditionalProperties();
    System.out.print(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema));
}

public static <T> ObjectSchema generateSchema(JsonSchemaGenerator generator, Class<T> type) throws JsonMappingException {
    ObjectSchema schema = generator.generateSchema(type).asObjectSchema();
    for (final Field field : type.getDeclaredFields()) {
        if (!field.getType().getName().startsWith("java") && !field.getType().isPrimitive()) {
            final ObjectSchema fieldSchema = generateSchema(generator, field.getType());
            fieldSchema.rejectAdditionalProperties();
            schema.putProperty(field.getName(), fieldSchema);
        }
    }
    return schema;
}
JEY
  • 6,973
  • 1
  • 36
  • 51
  • Thanks for answer but I don't want to put properties manually. Does Jackson have any api to provide that? – Beno Feb 28 '18 at 10:47
  • In my answer i suggested to use java reflection API to do it automatically. I don't know any other way to do it. – JEY Feb 28 '18 at 10:50
1

Well I would go to a simpler route if you don't want to use reflections. I would use JSONPath. So you would need to add below to your pom.xml

  <dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.3.0</version>
  </dependency>

Then below code demonstrates how to alter the generated JSON file

package taruntest;

import com.jayway.jsonpath.*;

public class Test {

    public static void main(String[] args) throws Exception {
        String data = "{\n" +
                "  \"type\" : \"object\",\n" +
                "  \"id\" : \"urn:jsonschema:com.xxx.xxx:A\",\n" +
                "  \"additionalProperties\" : false,\n" +
                "  \"properties\" : {\n" +
                "    \"s\" : {\n" +
                "      \"type\" : \"string\"\n" +
                "    },\n" +
                "    \"b\" : {\n" +
                "      \"type\" : \"object\",\n" +
                "      \"id\" : \"urn:jsonschema:com.xxx.xxx:B\",\n" +
                "      \"properties\" : {\n" +
                "        \"bd\" : {\n" +
                "          \"type\" : \"number\"\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}";

        DocumentContext doc = JsonPath.parse(data);
        doc.put("$..[?(@.id =~ /urn:jsonschema:.*/)]", "additionalProperties", false);
        String modified =  doc.jsonString();
        System.out.println(modified);
    }
}

The output of the run is (formatted manually)

{
  "type": "object",
  "id": "urn:jsonschema:com.xxx.xxx:A",
  "additionalProperties": false,
  "properties": {
    "s": {
      "type": "string"
    },
    "b": {
      "type": "object",
      "id": "urn:jsonschema:com.xxx.xxx:B",
      "properties": {
        "bd": {
          "type": "number"
        }
      },
      "additionalProperties": false
    }
  }
}
Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
1

The following worked for me:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kjetland.jackson.jsonSchema.JsonSchemaConfig;
import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;

...

ObjectMapper objectMapper = new ObjectMapper();
JsonSchemaConfig config = JsonSchemaConfig.nullableJsonSchemaDraft4();
JsonSchemaGenerator schemaGenerator = new JsonSchemaGenerator(objectMapper, config);
JsonNode jsonNode = schemaGenerator.generateJsonSchema(Test.class);
String jsonSchemaText = jsonNode.toString();

Using maven dependency:

<dependency>
    <groupId>com.kjetland</groupId>
    <artifactId>mbknor-jackson-jsonschema_2.12</artifactId>
    <version>1.0.28</version>
</dependency>

Using the following classes:

Test.java:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Test {
    @JsonProperty(required = true)
    private final String name;
    private final TestChild child;

    @JsonCreator
    public Test (
            @JsonProperty("name") String name,
            @JsonProperty("child") TestChild child) {
        this.name = name;
        this.child = child;
    }

    public String getName () {
        return name;
    }

    public TestChild getChild () {
        return child;
    }
}

...and TestChild.java:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class TestChild {
    @JsonProperty(required = true)
    private final String childName;

    @JsonCreator
    public TestChild (@JsonProperty("childName") String childName) {
        this.childName = childName;
    }

    public String getChildName () {
        return childName;
    }
}

Results in (output of jsonSchemaText piped through jq -C . for pretty formatting):

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Test",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "name": {
      "type": "string"
    },
    "child": {
      "oneOf": [
        {
          "type": "null",
          "title": "Not included"
        },
        {
          "$ref": "#/definitions/TestChild"
        }
      ]
    }
  },
  "required": [
    "name"
  ],
  "definitions": {
    "TestChild": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "childName": {
          "type": "string"
        }
      },
      "required": [
        "childName"
      ]
    }
  }
}

This results in "additionalProperties": false on both Test and TestChild.

Note: You can replace JsonSchemaConfig.nullableJsonSchemaDraft4() with JsonSchemaConfig.vanillaJsonSchemaDraft4() in your schema generation code to get rid of the "oneof" references with "type: null" or "type: ActualType" in favor of just "type: ActualType" (note, this still won't add them to the "required" array unless you annotate the properties with @JsonProperty(required = true)).

Shadow Man
  • 3,234
  • 1
  • 24
  • 35
  • Notice this also results in "reference" based schemas, as opposed to flattened schemas (which won't allow self-referencing structures such as Trees where a Branch may contain Branches and/or Leaves) such as shown in the example provided in the question. – Shadow Man Mar 06 '18 at 02:30
  • Thanks. I will try your solution and let you know. – Beno Mar 07 '18 at 09:32
  • It is the scala library. Are there the `java` library which provides same functionality ? – Beno Mar 07 '18 at 23:37
  • I'm using this and I'm not using scala at all. I'm using pure java. The library may have been written in scala, but it certainly works in java. Personally, I don't see this as something to use in a service somewhere. I see this as something to run in a maven plugin. We wrote such a plugin at my work to generate the schema from our model classes as part of the maven build process and upload it to an S3 bucket, so we don't even pull in those libs to our generated jars. – Shadow Man Mar 08 '18 at 01:50
1

This is my solution, without any reflect and hack way, and it works very well for me.

public static void rejectAdditionalProperties(JsonSchema jsonSchema) {
  if (jsonSchema.isObjectSchema()) {
    ObjectSchema objectSchema = jsonSchema.asObjectSchema();
    ObjectSchema.AdditionalProperties additionalProperties = objectSchema.getAdditionalProperties();
    if (additionalProperties instanceof ObjectSchema.SchemaAdditionalProperties) {
        rejectAdditionalProperties(((ObjectSchema.SchemaAdditionalProperties) additionalProperties).getJsonSchema());
    } else {
      for (JsonSchema property : objectSchema.getProperties().values()) {
        rejectAdditionalProperties(property);
      }
      objectSchema.rejectAdditionalProperties();
    }
  } else if (jsonSchema.isArraySchema()) {
    ArraySchema.Items items = jsonSchema.asArraySchema().getItems();
    if (items.isSingleItems()) {
      rejectAdditionalProperties(items.asSingleItems().getSchema());
    } else if (items.isArrayItems()) {
      for (JsonSchema schema : items.asArrayItems().getJsonSchemas()) {
        rejectAdditionalProperties(schema);
      }
    }
  }
}
Race
  • 384
  • 1
  • 11
  • 27