Problem:Sometimes an Access Request Target is a Single Target, Sometimes it is an array
Question: How can I make Jackson deserialize to either a Single Target or an Array depending on what is found?
Single Target JSON
"access_request": {
"targets": {
"target": {
"#type": "ANY",
"id": 1189118
}
}
}
Array of Target JSON
"access_request": {
"targets": {
"target": [
{
"#type": "Object",
"id": 1189167,
"object_name": "OUTSIDE",
"object_type": "acl",
"object_details": "2.2.2.2",
"management_id": 29,
"management_name": "ace2"
},
{
"#type": "Object",
"id": 1189168,
"object_name": "ONRAMP",
"object_type": "acl",
"object_details": "1.1.1.1",
"management_id": 29,
"management_name": "ace1"
}
]
}
}
'Target' POJO:
#JsonPropertyOrder({
"target"
})
#Generated("jsonschema2pojo")
public class Targets implements Serializable
{
#JsonProperty("target")
private Target target = new Target();//How can I make Jackson deserialize to either a Single Target or an Array<Target> depending on what is found?
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
.
.//Getters, Setters
.
}
Use a single field of type List, and activate the feature ACCEPT_SINGLE_VALUE_AS_ARRAY
YourTargetClass result = mapper.reader(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.forType(YourTargetClass.class)
.readValue(json);
Related
I'm writing a Spring Boot application in Kotlin, and I'm currently struggling to generate a specification for a DTO class that has a backing field of the type String, which I want to then later parse into one of two enum classes in the adapter layer.
I've tried the following approach using the oneOf Annotation value, which seemed like it does what I want:
data class MyDto(
#Schema(
type = "string",
oneOf = [MyFirstEnum::class, MySecondEnum::class]
)
val identifier: String,
val someOtherField: String
) {
fun transform() { ... } // this will use the string identifier to pick the correct enum type later
}
Which results in the following OpenApi Spec:
"MyDto": {
"required": [
"someOtherField",
"identifier"
],
"type": "object",
"properties": {
"identifier": {
"type": "object", // <--- this should be string
"oneOf": [{
"type": "string",
"enum": [
"FirstEnumValue1",
"FirstEnumValue2",
"FirstEnumValue3"
]
}, {
"type": "string",
"enum": [
"SecondEnumValue1",
"SecondEnumValue2",
"SecondEnumValue3"
]
}
]
},
"someOtherField": {
"type": "string"
}
}
}
As you can see, the enum constants are (I think) correctly inlined into the specification, but the type annotation on the field, which I set to string is bypassed, resulting in an object type, which I suppose is incorrect in this case.
My questions are:
Is my current code and the resulting spec valid with the object declaration instead of string?
Is there a better way to embed the enum values into the spec?
Edited to add: I'm using Spring Boot v2.7.8 in combination with springdoc-openapi v1.6.13 to automatically generate the OpenApi Spec.
The annotation based approach that I showed in my question does not seem to generate a valid OpenApi spec with springdoc-openapi:1.6.13. The type of the field identifier needs to be String, as Helen mentioned in the comments.
I was able to solve the issue by creating the Schema for this particular class manually, using a GlobalOpenApiCustomizer Bean:
#Bean
fun myDtoCustomizer(): GlobalOpenApiCustomizer {
val firstEnum = StringSchema()
firstEnum.description = "First Enum"
MyFirstEnum.values().forEach { firstEnum.addEnumItem(it.name) }
val secondEnum = StringSchema()
secondEnum.description = "Second Enum"
MySecondEnum.values().forEach { secondEnum.addEnumItem(it.name) }
return GlobalOpenApiCustomizer {
it.components.schemas[MyDto::class.simpleName] = ObjectSchema()
.addProperty(
MyDto::identifier.name,
StringSchema().oneOf(
listOf(
firstEnum,
secondEnum
)
)
)
.addProperty(MyDto::someOtherField.name, StringSchema())
}
}
Which in turn produces the following Spec:
"MyDto": {
"type": "object",
"properties": {
"identifier": {
"type": "string",
"oneOf": [{
"type": "string",
"description": "First Enum",
"enum": [
"FirstEnumValue1",
"FirstEnumValue2",
"FirstEnumValue3"
]
}, {
"type": "string",
"description": "Second Enum",
"enum": [
"SecondEnumValue1",
"SecondEnumValue2",
"SecondEnumValue3"
]
}
]
},
"someOtherField": {
"type": "string"
}
}
}
I am consuming an external web service and receiving a JSON response. In this response, there is an object "entities" containing multiple arrays in it, with a name before each array.
I want to add the name before the array in the array object itself.
For example this is the original response:
{
"entities": {
"entity": [
{
"confidence": 1,
"value": "user",
"type": "value"
},
{
"confidence": 1,
"value": "insurance form",
"type": "value"
}
],
"ui_page_step": [
{
"confidence": 1,
"value": "step 1",
"type": "value"
}
],
"userrole_ano": [
{
"confidence": 0.96535832252792,
"value": "anonymous user"
}
]
}
}
I need to convert it to:
{
"entities": {
"entity": [
{
"name": "entity",
"confidence": 1,
"value": "user",
"type": "value"
},
{
"name": "entity",
"confidence": 1,
"value": "insurance form",
"type": "value"
}
],
"ui_page_step": [
{
"name": "ui_page_step",
"confidence": 1,
"value": "step 1",
"type": "value"
}
],
"userrole_ano": [
{
"name": "userrole_ano",
"confidence": 0.96535832252792,
"value": "anonymous user"
}
]
}
}
How can I convert the original response to the desired one in Java?
Here is a (one of several possible) solutions:
It uses Jackson library to parse the Json into a java Map that is (relatively) easier to navigate and modify than JSONObject.
the method putCollectionNamesInsideEntries() assumes one root "entities" entry that has several collections as values. it iterates over all of them, adding "name" entry with name of collection.
the map is serialized back to Json (and sent to System.out)
import java.io.*;
import java.nio.file.*;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTest
{
public static void main(String[] args) {
try (InputStream is = Files.newInputStream(Paths.get("C:/temp/test.json"))) {
ObjectMapper mapper = new ObjectMapper();
// deserialize json into map
Map<String, Object> map = (Map<String, Object>)mapper.readValue(is, Map.class);
putCollectionNamesInsideEntries(map);
// serialize map into json
mapper.writeValue(System.out, map);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void putCollectionNamesInsideEntries(Map<String, Object> map) {
// get root "entities" entry
Map<String, Object> entitiesMap = (Map<String, Object>)map.get("entities");
for (Map.Entry<String, Object> entitiesEntry : entitiesMap.entrySet()) {
// iterate over collection entries
if (entitiesEntry.getValue() instanceof Collection) {
Collection coll = (Collection)entitiesEntry.getValue();
// iterate over entries in collection
for (Object collEntry : coll) {
if (collEntry instanceof Map) {
// add "name" with ame of collection (key entry under "entries")
((Map<String, Object>)collEntry).put("name", entitiesEntry.getKey());
}
}
}
}
}
}
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
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/
I am using an API where I supply an input string, and it returns some keyword autocompletions and product nodes.
My goal is to deserialize the response and get a list of the autocompletion Strings I can use. I'm trying implement this in an android application with the Retrofit library, which uses gson.
First off, I'm not sure the response I have is a typical JSON response. The 'nodes' item has key / value pairs, but the input string and the autocompletions list don't seem to have keys I can use.
["pol",
["polaroid camera",
"polo",
"polo ralph lauren",
"polo ralph lauren men",
"polar heart rate monitor",
"polaroid",
"polo shirt",
"polar watch",
"police scanner",
"polar"],
[{
"nodes": [{
"alias": "electronics",
"name": "Electronics"
},
{
"alias": "electronics-tradein",
"name": "Electronics Trade-In"
}]
},
{
},
{
},
{
},
{
},
{
},
{
},
{
},
{
},
{
}],
[]]
This is my attempt at the java classes for gson to deserialize to. However, it doesn't work as from what I understand, gson needs the class variables to match the JSON keys (true for Node class but not the rest).
class Response {
String input;
List<String> keywords;
List<Node> nodes;
}
class Node {
String alias;
String name;
}
the json only has a couple of keys in it, this is largely a Json Array.
if you can change the JSON, make it more like this
{
"input" : "pol",
"keywords" : ["polaroid camera","polo",...],
"nodes": [{
"alias": "electronics",
"name": "Electronics"
},
{
"alias": "electronics-tradein",
"name": "Electronics Trade-In"
}]
}