Java: Deserialize composite JSON Schema with $ref to one single schema - java

According to the Structuring a complex schema, it's possible to have the following relation:
Base JSON Schema (customer.json)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"billing_address": { "$ref": "address.json" }
}
}
Referenced JSON Schema (address.json)
{
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
},
"required": ["street_address", "city", "state"]
}
The key advantage of such an approach is reusability.
The issue appears if I want to compose these schemas into one. For example, I need to generate a JSON file with dummy values for all supported fields.
So, I want to have this schema as a result:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"billing_address": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
}
}
}
Note that all schemas are present in the classpath.
I've been searching for existing solutions on how to do it in Java.
Unfortunately, most of the libraries solve the tasks of how to generate schema by POJO. But in this case, I need the reverse one

There are generators in both directions:
from POJO to Schema
from Schema to POJO
You seem to be interested in neither since what you want is:
from Schema (parts) to (single) Schema
I'm afraid your chances of finding an existing solution may be small.
But you should be able to do this yourself, especially if you can make a couple of simplifying assumptions:
Nowhere in your data model do you have properties with the name $ref.
All your schema parts are present on the classpath – for simplicity's sake: in the same package as the java class performing
the merge of the separate schema part.
There is no circular reference to your main/entry schema from one of its referenced other schema parts.
It is acceptable to include the different parts in the entry schema's definitions.
The schema parts do not have overlapping definitions.
The utility could look something like this then:
public class SchemaMerger {
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, ObjectNode> schemas = new HashMap<>();
private final List<ObjectNode> definitions = new ArrayList<>();
public String createConsolidatedSchema(String entrySchemaPath) throws IOException {
ObjectNode entrySchema = this.getSchemaWithResolvedParts(entrySchemaPath);
ObjectNode consolidatedSchema = this.objectMapper.createObjectNode().setAll(entrySchema);
ObjectNode definitionsNode = consolidatedSchema.with("definitions");
this.definitions.forEach(definitionsNode::setAll);
for (Map.Entry<String, ObjectNode> schemaPart : this.schemas.entrySet()) {
// include schema loaded from separate file in definitions
definitionsNode.set(schemaPart.getKey(), schemaPart.getValue().without("$schema"));
}
return consolidatedSchema.toPrettyString();
}
private ObjectNode getSchemaWithResolvedParts(String schemaPath) throws IOException {
ObjectNode entrySchema = (ObjectNode) this.objectMapper.readTree(SchemaMerger.loadResource(schemaPath));
this.resolveExternalReferences(entrySchema);
JsonNode definitionsNode = entrySchema.get("definitions");
if (definitionsNode instanceof ObjectNode) {
this.definitions.add((ObjectNode) definitionsNode);
entrySchema.remove("definitions");
}
return entrySchema;
}
private void resolveExternalReferences(JsonNode schemaPart) throws IOException {
if (schemaPart instanceof ObjectNode || schemaPart instanceof ArrayNode) {
// recursively iterate over all nested nodes
for (JsonNode field : schemaPart) {
this.resolveExternalReferences(field);
}
}
if (!(schemaPart instanceof ObjectNode)) {
return;
}
JsonNode reference = schemaPart.get("$ref");
if (reference instanceof TextNode) {
String referenceValue = reference.textValue();
if (!referenceValue.startsWith("#")) {
// convert reference to separate file to entry in definitions
((ObjectNode) schemaPart).put("$ref", "#/definitions/" + referenceValue);
if (!this.schemas.containsKey(referenceValue)) {
this.schemas.put(referenceValue, this.getSchemaWithResolvedParts(referenceValue));
}
}
}
}
private static String loadResource(String resourcePath) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = SchemaMerger.class.getResourceAsStream(resourcePath);
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
while (scanner.hasNext()) {
stringBuilder.append(scanner.nextLine()).append('\n');
}
}
return stringBuilder.toString();
}
}
Calling new SchemaMerger().createConsolidatedSchema("customer.json") results in the following schema being generated:
{
"$schema" : "http://json-schema.org/draft-07/schema#",
"type" : "object",
"properties" : {
"billing_address" : {
"$ref" : "#/definitions/address.json"
}
},
"definitions" : {
"address.json" : {
"type" : "object",
"properties" : {
"street_address" : {
"type" : "string"
},
"city" : {
"type" : "string"
},
"state" : {
"type" : "string"
}
},
"required" : [ "street_address", "city", "state" ]
}
}
}
This should give you a starting point from which to build what you need.

Refer: this post. I have posted a possible solution.
Though, as mentioned, i have not yet tried it out myself.

Related

Is there a way to extract JSON property labels from a JSON schema using Gson?

What I want to do is have Gson Type Adapters that don't have to have the property labels for a given JSON object hard coded into the adapter, but instead pull the Strings from the JSON Schema.
So if I have a schema like this (borrowing from json-schema.org):
{
"$id": "/schemas/address",
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" }
}
}
Is there a way to extract the property names "street_address", "city", and "state" from the schema and assign them to variables in a Gson TypeAdapter or Factory such that I don't have to declare a String like
String streetName = "street_address";
But could instead do something in the vein of
String streetName = getSchemaProperty("/schemas/address").getProperty(0);
Where getSchemaProperty() would get the object schema from the "/schemas/address" file and getProperty would return the first property label in the schema. That way, if the schema was updated with new property labels, I would not have to update the Adapters with new Strings.
I can certainly write something that would do this (parse the schema file(s) and extract that information), I'm just wondering if that kind of work has already been done (maybe with annotations or some such?) & I'm just missing it?
Easier than I thought, actually, since a Schema is a JSON file.
Here is an example Schema:
{
"$id": "/path/to/schema/MySchema",
"$schema": "http://json-schema.org/draft/2019-09/schema#",
"title": "My Example JSON Schema",
"description": "JSON Schema for me",
"$comment": "Hey! A Comment!",
"$defs" :
{
"My Class":
{
"$comment": "Another Comment! I love comments, so useful!",
"type": "object",
"required": [ "Value", "Description" ],
"properties":
{
"Value": { "type": "number" },
"Description": { "type": "string" }
}
}
}
}
Here is the default constructor of my Google GSON Type Adapter:
public MyTypeAdapter()
{
InputStream is = getClass().getResourceAsStream("/path/to/schema/MySchema.json");
InputStreamReader isr= new InputStreamReader(is, "UTF-8");
JsonObject schemaJO= JsonParser.parseReader(isr).getAsJsonObject;
JsonObject thisJO = schemaJO.getAsJsonObject("$defs").getAsJsonObject("My Class").getAsJsonObject("properties");
//Get the list of property labels\names\keys
Set<String> keySet = thisJO .keySet();
//loop over the set and distribute the Strings as desired.
}
Easy Peasy Lemon Squeezy!

How to write custom json serializer and serialize value as an array

I need to serialize simple java object with three fields (one is a List of objects) into json to look something like this:
{
"id": "1",
"fields": [
{
"value": {
"someNumber": "0.0.2"
},
"id": "67"
}
],
"name": "Daniel"}
I've read guides on custom serializers StdSerializer and JsonGenerator, as i undestood, to write "name": "Daniel" into json you need to do somwthing like gen.writeObjectField("name", name); but i cannot get my head on two things:
How to write some string value like here:
"value": {
"name": "0.0.2"
},
And how to write java List as an array like this:
"fields": [
{
"value": {
"someNumber": "0.0.2"
},
"id": "67"
}]
where "fields" is an List full of objects having two fields: "value" and "id".
Any help is appreciated
like this
public class Test {
public static void main(String[] args) throws IOException {
String ret;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper mapper = new ObjectMapper();
JsonGenerator jg = mapper.getJsonFactory().createJsonGenerator(new PrintWriter(bos));
jg.writeStartObject();
jg.writeStringField("id", "1");
jg.writeArrayFieldStart("fields");
jg.writeStartObject();
jg.writeObjectFieldStart("value");
jg.writeStringField("someNumber","0.0.2");
jg.writeEndObject();
jg.writeStringField("id","67");
jg.writeEndObject();
//you can write more objects in fields here
jg.writeEndArray();
jg.writeStringField("name","Daniel");
jg.writeEndObject();
jg.flush();
jg.close();
ret = bos.toString();
bos.close();
System.out.println(ret);
}
}
and the result is
{
"id":"1",
"fields":[
{
"value":{
"someNumber":"0.0.2"
},
"id":"67"
}
],
"name":"Daniel"
}

JSON Path: how to convert URN references to local references

Whilst converting a JSON schema file to Java classes using the Maven org.jsonschema2pojo:jsonschema2pojo-maven-plugin:1.0.0-alpha2 I am getting errors related to urn references which cannot be resolved.
Here is a sample error message:
[ERROR] Failed to execute goal org.jsonschema2pojo:jsonschema2pojo-maven-plugin:1.0.0-alpha2:generate (default) on project model-reservation: Execution default of goal org.jsonschema2pojo:jsonschema
2pojo-maven-plugin:1.0.0-alpha2:generate failed: Unrecognised URI, can't resolve this: urn:jsonschema:com:lumina:pnr:model:Reference: unknown protocol: urn -> [Help 1]
Here is the JSON it refers to with the $ref element which is causing the exception:
"remarkFields": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"id": "urn:jsonschema:com:lumina:pnr:model:FileFinishingField",
"properties": {
"lineNumber": {
"type": "integer"
},
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"references": {
"type": "array",
"items": {
"type": "object",
"$ref": "urn:jsonschema:com:lumina:pnr:model:Reference"
}
}
}
}
}
}
How can I convert these to local references in Java and use the conversion output to successfully convert my code to Java classes using the org.jsonschema2pojo:jsonschema2pojo-maven-plugin:1.0.0-alpha2 Maven plugin?
There might be better ways to solve this problem, yet one possible solution is this one:
Use a recursive method to convert the urn style references to local references:
Here is the recursive method using the Jackson library:
private static final String ITEMS = "items";
private static final String ID = "id";
private static final String PROPERTIES = "properties";
private static final String ADDITIONAL_PROPERTIES = "additionalProperties";
private static final String REF = "$ref";
private static void parseReferences(JsonNode jsonNode, String path) {
if (jsonNode.has(ID)) {
typeMap.put(jsonNode.get(ID).asText(), path);
final JsonNode properties = jsonNode.get(PROPERTIES);
final Iterator<Map.Entry<String, JsonNode>> fields = properties.fields();
path += "/" + PROPERTIES;
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
parseReferences(entry.getValue(), path + "/" + entry.getKey());
}
} else if (jsonNode.has(ITEMS)) {
final JsonNode item = jsonNode.get(ITEMS);
parseReferences(item, path + "/" + ITEMS);
} else if (jsonNode.has(REF)) {
ObjectNode objectNode = (ObjectNode) jsonNode;
objectNode.set(REF, new TextNode(typeMap.get(jsonNode.get(REF).asText())));
} else if (jsonNode.has(ADDITIONAL_PROPERTIES)) {
JsonNode additionalProperties = jsonNode.get(ADDITIONAL_PROPERTIES);
parseReferences(additionalProperties, path + "/" + ADDITIONAL_PROPERTIES);
}
}
This is how I am calling this method after having instantiated a JAXB parser:
private static void writeSchemaToFile(ObjectMapper jaxbObjectMapper, String origPath, String path) throws Exception {
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(origPath);
try (Reader r = new InputStreamReader(resourceAsStream, "UTF-8")) {
JsonNode root = jaxbObjectMapper.readTree(r);
parseReferences(root, "#");
String changedJson = jaxbObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(root);
final Path targetPath = Paths.get(path);
if (!Files.exists(targetPath)) {
Path parent = targetPath.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
}
try (Writer writer = Files.newBufferedWriter(targetPath, Charset.forName("UTF-8"), StandardOpenOption.CREATE)) {
writer.write(changedJson);
}
}
}
If you call this method, it will convert the JSON specified in the question to:
"remarkFields" : {
"type" : "object",
"additionalProperties" : {
"type" : "array",
"items" : {
"type" : "object",
"id" : "urn:jsonschema:com:lumina:pnr:model:FileFinishingField",
"properties" : {
"lineNumber" : {
"type" : "integer"
},
"name" : {
"type" : "string"
},
"value" : {
"type" : "string"
},
"references" : {
"type" : "array",
"items" : {
"type" : "object",
"$ref" : "#/properties/passengers/items/properties/reference"
}
}
}
}
}
}

JSONSchema parsing and processing in Java

There is a perfect .NET library Json.NET Schema. I use it in my C# application to parse schemas and make a Dictionary<string, JSchema> with pairs "name_of_simple_element" - "simple_element". Then I process each pair and for example try to find "string" type elements with pattern "[a-z]" or "string" elements with maximumLength > 300.
Now I should create application with same functions in Java. It is very simple in C#:
Jschema schema = JSchema.Parse(string json);
IDictionary<string, JSchema> dict = schema.Properties;
... etc.
But i cant find same way to do that in Java. I need to convert this
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://iitrust.ru",
"type": "object",
"properties": {
"regions": {
"id": "...regions",
"type": "array",
"items": {
"id": "http://iitrust.ru/regions/0",
"type": "object",
"properties": {
"id": {
"id": "...id",
"type": "string",
"pattern": "^[0-9]+$",
"description": "Идентификатор региона"
},
"name": {
"id": "...name",
"type": "string",
"maxLength": 255,
"description": "Наименование региона"
},
"code": {
"id": "...code",
"type": "string",
"pattern": "^[0-9]{1,3}$",
"description": "Код региона"
}
},
"additionalProperties": false,
"required": ["id",
"name",
"code"]
}
}
},
"additionalProperties": false,
"required": ["regions"]
}
to pseudocode dictionary/map like this
["...id" : "id": { ... };
"...name" : "name": { ... };
"...code": "code": { ... }]
What is the best way to do that?
Ok, problem is resolved by Jackson library. Code below is based on generally accepted rule that JSON Schema object is always has a "properties" element, "array" node is always has a "items" element, "id" is always unique. This is my customer's standart format. Instead of a C#'s Dictionary<string, Jschema> I have got a Java's HashMap<String, JsonNode>.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
...
static Map<String, JsonNode> elementsMap = new HashMap<>();
public static void Execute(File file) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(file);
JsonNode rootNode = root.path("properties");
FillTheElementMap(rootNode);
}
private static void FillTheElementMap(JsonNode rootNode) {
for (JsonNode cNode : rootNode){
if(cNode.path("type").toString().toLowerCase().contains("array")){
for(JsonNode ccNode : cNode.path("items")){
FillTheElementMap(ccNode);
}
}
else if(cNode.path("type").toString().toLowerCase().contains("object")){
FillTheElementMap(cNode.path("properties");
}
else{
elementsMap.put(cNode.path("id").asText(), cNode);
}
}
A good option for you should be this Java implementation of JSONPath.
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONObject;
...
DocumentContext context = JsonPath.parse(jsonSchemaFile);
//"string" elements with maximumLength == 255
List<Map<String, JSONObject>> arr2 = context.read(
"$..[?(#.type == 'string' && #.maxLength == 255)]");
And if you want to create a JsonSchema from Java code, you could use jackson-module-jsonSchema.
If you want to validate a JsonSchema, then the library from fge is an option: json-schema-validator
You may want to take a look at this library, it's helped me with similar requirements. With a couple lines of code you can traverse a pretty straightforward Java object model that describes a JSON schema.
https://github.com/jimblackler/jsonschemafriend (Apache 2.0 license)
From the README:
jsonschemafriend is a JSON Schema loader and validator, delivered as a Java library...
It is compatible with the following metaschemas
http://json-schema.org/draft-03/schema#
http://json-schema.org/draft-04/schema#
http://json-schema.org/draft-06/schema#
http://json-schema.org/draft-07/schema#
https://json-schema.org/draft/2019-09/schema

Deserialize JSON with unknown Keys

I'm trying to deserialize a JSON object (from JIRA REST API createMeta) with unknown keys.
{
"expand": "projects",
"projects": [
{
"self": "http://www.example.com/jira/rest/api/2/project/EX",
"id": "10000",
"key": "EX",
"name": "Example Project",
"avatarUrls": {
"24x24": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10000&avatarId=10011",
"16x16": "http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000&avatarId=10011",
"32x32": "http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000&avatarId=10011",
"48x48": "http://www.example.com/jira/secure/projectavatar?pid=10000&avatarId=10011"
},
"issuetypes": [
{
"self": "http://www.example.com/jira/rest/api/2/issueType/1",
"id": "1",
"description": "An error in the code",
"iconUrl": "http://www.example.com/jira/images/icons/issuetypes/bug.png",
"name": "Bug",
"subtask": false,
"fields": {
"issuetype": {
"required": true,
"name": "Issue Type",
"hasDefaultValue": false,
"operations": [
"set"
]
}
}
}
]
}
]
}
My problem is: I don't know the keys into "fields" (in the example below "issuetype", "summary", "description", "customfield_12345").
"fields": {
"issuetype": { ... },
"summary": { ... },
"description": { ... },
"customfield_12345": { ... }
}
It would be awesome if I could deserialize it as an array with the key as "id" in my POJO so the above example will looke like the following:
class IssueType {
...
public List<Field> fields;
...
}
class Field {
public String id; // the key from the JSON object e.g. "issuetype"
public boolean required;
public String name;
...
}
Is there a way I can achieve this and wrap in my model? I hope my problem is somehow understandable :)
If you don't know the keys beforehand, you can't define the appropriate fields. The best you can do is use a Map<String,Object>.
If there are in fact a handful of types, for which you can identify a collection of fields, you could write a custom deserializer to inspect the fields and return an object of the appropriate type.
I know it's old question but I also had problem with this and there are results..
Meybe will help someone in future : )
My Response with unknow keys:
in Model Class
private JsonElement attributes;
"attributes": {
"16": [],
"24": {
"165": "50000 H",
"166": "900 lm",
"167": "b.neutr.",
"168": "SMD 3528",
"169": "G 13",
"170": "10 W",
"171": "230V AC / 50Hz"
}
},
So I also checked if jsonElement is jsonArray its empty.
If is jsonObject we have data.
ProductModel productModel = productModels.get(position);
TreeMap<String, String> attrsHashMap = new TreeMap<>();
if (productModel.getAttributes().isJsonObject())
{
for (Map.Entry<String,JsonElement> entry : productModel.getAttributes().getAsJsonObject().entrySet())
{
Log.e("KEYS", "KEYS: " + entry.getKey() + " is empty: " + entry.getValue().isJsonArray());
if (entry.getValue() != null && entry.getValue().isJsonObject())
{
for (Map.Entry<String, JsonElement> entry1 : entry.getValue().getAsJsonObject().entrySet())
{
Log.e("KEYS", "KEYS INSIDE: " + entry1.getKey() + " VALUE: " + entry1.getValue().getAsString());
// and there is my keys and values.. in your case You can get it in upper for loop..
}
}
}
There is a perfectly adequate JSON library for Java that will convert any valid JSON into Java POJOs. http://www.json.org/java/

Categories