Beno
Beno

Reputation: 987

Jackson: How can I generate json schema which rejects all additional content

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

Upvotes: 16

Views: 7326

Answers (4)

Race
Race

Reputation: 414

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);
      }
    }
  }
}

Upvotes: 1

Shadow Man
Shadow Man

Reputation: 3402

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)).

Upvotes: 1

Tarun Lalwani
Tarun Lalwani

Reputation: 146490

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
    }
  }
}

Upvotes: 1

JEY
JEY

Reputation: 7123

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;
}

Upvotes: 4

Related Questions