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

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

Related

Json deserialization with key value mapped object

I'm trying to deserialize a json string to Java object. Here is my json string.
{
"header": {
"transactionId": "12345",
"application": "testApp"
},
"items": {
"item": [
{
"attributes": {
"attribute": [
{
"key": "accountType",
"value": "TYPE1"
},
{
"key": "accountId",
"value": "123"
}
]
}
},
{
"attributes": {
"attribute": [
{
"key": "userType",
"value": "TYPE2"
},
{
"key": "userId",
"value": "321"
}
]
}
}
]
}
}
And I want to deserialize this json to Java classes that shown as below.
public class Response {
private Header header;
private List<Object> items;
//getters and setters
}
public class Header {
private String transactionId;
private String application;
//getters and setters
}
public class Account {
private String accountType;
private String accountId;
//getters and setters
}
public class User {
private String userType;
private String userId;
//getters and setters
}
How can I deserialize by using jackson or objectmapper etc.? The main problem is the field names are in the attribute object and the value of 'key' field. Is it possible to find the right field on Java object by using value of the 'key' field and to set the right value with the value of the 'value' field?
One possible solution is to transform the JSON structure before converting to POJO.
https://github.com/octomix/josson
Josson josson = Josson.fromJsonString(yourJsonString);
JsonNode node = josson.getNode(
"map(header," +
" items: items.item#" +
" .attributes.attribute" +
" .map(key::value)" +
" .mergeObjects()" +
" )");
System.out.println(node.toPrettyString());
Output
{
"header" : {
"transactionId" : "12345",
"application" : "testApp"
},
"items" : [ {
"accountType" : "TYPE1",
"accountId" : "123"
}, {
"userType" : "TYPE2",
"userId" : "321"
} ]
}

Serialize Java Object attribute to JSON

I have an API built in Java Spring that return (using JacksonJaxbJsonProvider 2.5.5) a JSON object from this class:
public class FieldValues {
private String code;
private Object value;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
In the main object I've
#JsonRootName(value = "WorkRequest")
#XmlRootElement(name = "WorkRequest")
#JsonIgnoreProperties(ignoreUnknown = true)
public class WorkRequestDTOResponse {
private List<FieldValues> fieldValues;
public List<FieldValues> getFieldValues() {
return fieldValues;
}
public void setFieldValues(List<FieldValues> fieldValues) {
this.fieldValues = fieldValues;
}
}
But the output of the fieldValues object is this:
"fieldValues": [
{
"code": "anomaly",
"value": {
"#xsi.type": "ns3:boolean",
"$": "true"
}
},{
"code": "internal_note",
"value": {
"#xsi.type": "ns3:string",
"$": "Test text example"
}
}
]
instead what I need is this:
"fieldValues": [
{
"code": "anomaly",
"value": true
},{
"code": "internal_note",
"value": "Test text example"
}
]
This is my JSON Provider:
public class ErmesJSONProvider extends JacksonJaxbJsonProvider {
public ErmesJSONProvider() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.configure(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, false);
_mapperConfig.setMapper(mapper);
_mapperConfig.getConfiguredMapper().setAnnotationIntrospector(new JacksonAnnotationIntrospector());
}
}
Trying to use a String instead an object:
public class FieldValues {
private String code;
private String value;
But if I set this value as String fieldValues.setValue("true"), the JSON output is "value": true instead "value": "true"
Likewise if I set this value as String but with an Integer fieldValues.setValue("1"), the JSON output is "value": 1 instead "value": "1"
If I print the return object using ObjectMapper I've the right JSON:
String payload = new ObjectMapper().writeValueAsString(requestResult)
but if I return a Response like this:
return Response.status(Response.Status.CREATED).entity(new GenericEntity<RequestResult>(requestResult){}).build()
it return the wrong JSON.
I can't understand why 😥
Someone can help me? Thanks.

Aggregation lookup not working in spring

I am running the following aggregation lookup query in spring but it doesn't seems to output the same result as one i run in mongo shell. I am guessing mongo shell knows that from:"testModel" collection exists but how do spring know "testModel" exists as we are only pass strings in Aggregation.lookup("testModel"). I can run all other stages fine for example match or project..
// code running in Mongo Shell
db.demoModel.aggregate([{
$lookup:
{
from: "testModel",
localField: "_id",
foreignField: "makeId",
as: "cool"
}
}]
)
// code from spring
public List<DemoModel> getSomeAutos() throws Exception {
LookupOperation lookupStage = Aggregation.lookup(
"testModel",
"_id",
"makeId",
"cool"
);
Aggregation aggregation = Aggregation.newAggregation(lookupStage);
List<DemoModel> demomode = mongoTemplate.aggregate(aggregation, "demoModel", DemoModel.class).getMappedResults();
return demomode;
}
// DemoModel.class
public class DemoModel {
#Id
private String id;
private String value;
private String label;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
//Mongo demoModel Collection
{
"_id" : ObjectId("5a8ee0d815dc17aa32f90f4b"),
"value" : "Mitsubishi",
"label" : "Mitsubishi"
}
{
"_id" : ObjectId("5a8ee0d815dc17aa32f90f4c"),
"value" : "BMW",
"label" : "BMW"
}
// Mongo testModel Collection
{
"_id" : ObjectId("5a8ee393b80c346266f25aba"),
"make" : "Mitsubishi",
"label" : "3000GT",
"value" : "3000GT",
"makeId" : ObjectId("5a8ee0d815dc17aa32f90f4b")
}
{
"_id" : ObjectId("5a8ee393b80c346266f25af8"),
"make" : "BMW",
"label" : "Alpina A2",
"value" : "Alpina A2",
"makeId" : ObjectId("5a8ee0d815dc17aa32f90f4c")
}
// Response i am getting
[
{
"id": "5a8ee0d815dc17aa32f90f4b",
"value": "Mitsubishi",
"label": "Mitsubishi"
},
{
"id": "5a8ee0d815dc17aa32f90f4c",
"value": "BMW",
"label": "BMW"
}
]
// expected response in this format
{
"_id" : ObjectId("5a8ee0d815dc17aa32f90f4b"),
"value" : "Mitsubishi",
"label" : "Mitsubishi",
"cool" : [
{
"_id" : ObjectId("5a8ee393b80c346266f25aba"),
"make" : "Mitsubishi",
"label" : "3000GT",
"value" : "3000GT",
"makeId" : ObjectId("5a8ee0d815dc17aa32f90f4b")
}]
}

deserialize a json array into a java object holding list

I'm getting a json array holding objects looking like this:
[
{
"id": "1",
"name": "some name",
"url": "some url",
"active": true
}, {
"id": "2",
"name": "some other name",
"url": "some other url",
"active": true
}
]
Now, I want to be able to deserialize that array into a java object holding a list of the objects in the array. I have made a custom deserializer looking like this:
public class ListSerializer extends JsonDeserializer<List<Provider>>{
private static final long serialVersionUID = 9114152571639338391L;
#Override
public List<Provider> deserialize(JsonParser jsonParser,
DeserializationContext arg1) throws IOException, JsonProcessingException {
// TODO Auto-generated method stub
final ObjectCodec objectCodec = jsonParser.getCodec();
final JsonNode listOrObjectNode = objectCodec.readTree(jsonParser);
final List<Provider> result = new ArrayList<Provider>();
for (JsonNode node : listOrObjectNode) {
result.add(objectCodec.treeToValue(node, Provider.class));
}
return result;
}
}
And the class holding the list looks like this:
public class ProviderList {
#JsonDeserialize(using = ListSerializer.class)
private List<Provider> providerList;
public List<Provider> getProviderList() {
return providerList;
}
public void setProviderList(final List<Provider> providerList) {
this.providerList = providerList;
}
}
I am obviously doing something wrong, because I'm getting this error:
Can not deserialize instance of
com.wirelesscar.trailser.v1_0.domain.ProviderList out of START_ARRAY
token at [Source:
[{"id":"1","name":"Posttrack","url":"http:\dev.posttrack.com","active":true},{"id":"2","name":"Trackunit","url":"http:\dev.trackunit.com","active":true}];
line: 1, column: 1]
How can I do this properly?
You can deserialize directly to a list by using the TypeReference wrapper.
#Data
public class Provider {
private Long id;
private String name;
private String url;
private boolean active;
}
#Data
public class ProviderList {
List<Provider> providerList;
}
public class JsonTest {
#Test
public void test() {
String json = "[{\n" +
" \"id\": \"1\",\n" +
" \"name\": \"some name\",\n" +
" \"url\": \"some url\",\n" +
" \"active\": true\n" +
" }, {\n" +
" \"id\": \"2\",\n" +
" \"name\": \"some other name\",\n" +
" \"url\": \"some other url\",\n" +
" \"active\": true\n" +
" }\n" +
"]";
ObjectMapper mapper = new ObjectMapper();
try {
List<Provider> providerList = mapper.readValue(json, new TypeReference<List<Provider>>(){});
for (Provider provider : providerList) {
System.out.println(provider);
}
ProviderList list = new ProviderList();
list.setProviderList(providerList);
} catch (IOException e) {
e.printStackTrace();
}
}
}

Creating POJOs to match a JSON structure

I have devised a JSON structure to represent a table with header columns plus table rows that looks like the following.
{
"header": [
{
"fieldType": "STRING",
"readOnly": true,
"headerValue": "name"
},
{
"fieldType": "STRING",
"readOnly": true,
"headerValue": "description"
}
],
"rows": [
[
{
"fieldValue" : "engine"
},
{
"fieldValue" : "this is an engine"
}
],
[
{
"fieldValue" : "engine"
},
{
"fieldValue" : "this is an engine"
}
],
[
{
"fieldValue" : "engine"
},
{
"fieldValue" : "this is an engine"
}
],
[
{
"fieldValue" : "engine"
},
{
"fieldValue" : "this is an engine"
}
]
]
}
A row is for example
[
{
"fieldValue" : "engine"
},
{
"fieldValue" : "this is an engine"
}
]
The number of entries in a row matches the number of header columns. So "engine" is the "name" column and "this is an engine" is the "description" column
When I use GSON to turn my POJO's into a JSON String the closest I have got to match this structure is:
{
"header": [
{
"fieldType": "STRING",
"readOnly": true,
"headerValue": "name"
},
{
"fieldType": "STRING",
"readOnly": true,
"headerValue": "description"
}
],
"rows": [
{
"fieldValues": [
"engine",
"this is an engine"
]
},
{
"fieldValues": [
"engine",
"this is an engine"
]
},
{
"fieldValues": [
"engine",
"this is an engine"
]
},
{
"fieldValues": [
"engine",
"this is an engine"
]
}
]
}
Here's the code I'm using to test
enum FieldType {
STRING,
BOOLEAN,
NUMBER,
PHOTO,
PHOTOLIST;
}
class SurveyFields {
private List<SurveyColumn> header;
private List<SurveyRow> rows;
public List<SurveyColumn> getHeader() {
return header;
}
public List<SurveyRow> getRows() {
return rows;
}
public void setHeader(List<SurveyColumn> header) {
this.header = header;
}
public void setRows(List<SurveyRow> rows) {
this.rows = rows;
}
}
class SurveyColumn {
private FieldType fieldType;
private boolean readOnly;
private String headerValue;
public static class Builder {
private FieldType fieldType;
private boolean readOnly;
private String headerValue;
public Builder withFieldType(FieldType fieldType) {
this.fieldType = fieldType;
return this;
}
public Builder withReadOnly(boolean readOnly) {
this.readOnly = readOnly;
return this;
}
public Builder withHeaderValue(String headerValue) {
this.headerValue = headerValue;
return this;
}
public SurveyColumn build() {
return new SurveyColumn(fieldType, readOnly, headerValue);
}
}
public SurveyColumn(FieldType fieldType, boolean readOnly, String headerValue) {
this.fieldType = fieldType;
this.readOnly = readOnly;
this.headerValue = headerValue;
}
}
class SurveyRow {
public static class Builder {
private String[] fieldValues;
public Builder withFieldValues(String[] fieldValues) {
this.fieldValues = fieldValues;
return this;
}
public SurveyRow build() {
return new SurveyRow(fieldValues);
}
}
private String[] fieldValues;
public SurveyRow(String[] fieldValues) {
this.fieldValues = fieldValues;
}
}
public class TestGson {
public static void main(String[] args) {
SurveyFields fields = new SurveyFields();
fields.setHeader(Arrays.asList(new SurveyColumn[] {
new SurveyColumn.Builder().withHeaderValue("name").withFieldType(FieldType.STRING).withReadOnly(true)
.build(),
new SurveyColumn.Builder().withHeaderValue("description").withFieldType(FieldType.STRING)
.withReadOnly(true).build() }));
fields.setRows(Arrays.asList(new SurveyRow[] {
new SurveyRow.Builder().withFieldValues(new String[] { "engine", "this is an engine" }).build(),
new SurveyRow.Builder().withFieldValues(new String[] { "engine", "this is an engine" }).build(),
new SurveyRow.Builder().withFieldValues(new String[] { "engine", "this is an engine" }).build(),
new SurveyRow.Builder().withFieldValues(new String[] { "engine", "this is an engine" }).build()
}));
Gson gson = new Gson();
System.out.println(gson.toJson(fields));
}
}
How can I structure my POJO's to match the expected JSON output?
If you get the JSON from a third party, this site might help you generate POJO from JSON.

Categories

Resources