简体   繁体   中英

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

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

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

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

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

You can achieve this by impplementing Interface in those classes:

public interface I {
    boolean additionalProperties  = false;
}

public class A implements I {
    private String s;
    private B b;

    public String getS() {
        return s;
    }

    public B getB() {
        return b;
    }
}

public class B implements I {
    private BigDecimal bd;

    public BigDecimal getBd() {
        return bd;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM