PathNotFoundException: Missing property in path while iterating over recursive JSON structure - java

I have a JSON structure where some objects can have arbitrary names and i want to normalize this structure, such that the objects become anonymous, but can be identified by a "name" property.
From
{
{
"arbitrary_name": {
...
}
}
to
{
"name": "arbitrary_name",
...
}
For the first step, i want to add a name property to the "arbitrary-name" object and in the next step remove the "arbitrary-name" object and move it's properties one level up.
First, i'm querying the JSON structure (using https://github.com/json-path/JsonPath) for all paths pointing to objects, which need to be normalized. They are all positioned under an array with name "children". Then i'm iterating this paths, get the objects' values and add a name property.
My approach works fine on the top-level object, but if they are nested (child objects -> service.children[0].arbitrary_name1.children[0]), JsonPath throws an exception and i don't get it what property is missing. I printed all possible paths in this structure and the queried path (which causes the exception) is printed too.
My input looks like this:
{
"service": {
"foo": "bar",
"children": [
{
"arbitrary_name1": {
"foo": "bar",
"children": [
{
"arbitrary_name_2": {
"foo": "bar"
}
}
]
}
}
]
}
}
My code:
final File src = Files.readString(xxx); // Path to file with JSON content
final Configuration c = Configuration.defaultConfiguration().setOptions(Option.ALWAYS_RETURN_LIST, Option.AS_PATH_LIST);
final DocumentContext pathsContext = JsonPath.parse(src, c);
final DocumentContext valuesContext = JsonPath.parse(src);
final Collection<String> pathsToObjects = pathsContext.read("$..children[*]");
/* prints:
$['service']['children'][0]
$['service']['children'][0]['arbitrary_name1']['children'][0]
*/
for (String jsonPath : pathsToObjects) {
Map<String, Map> valuesOfOuterObject = valuesContext.read(jsonPath); // Exception on 2nd path
String name = valuesOfOuterObject.keySet().stream().findFirst().get();
final Map<String, Object> valuesOfInnerObject = valuesOfOuterObject.get(name);
valuesOfInnerObject.put("name", name);
valuesContext.set(jsonPath, valuesOfInnerObject);
}
The corresponding exception says:
com.jayway.jsonpath.PathNotFoundException: Missing property in path $['service']['children'][0]['arbitrary_name1']
What am i missing or is there an easier approach?

Related

Parse json contains array and object

I have JSON from payfort to read the transactions, tried to parse it to POJO but always gave to me the mismatch erorr
[
[
{
"response_code": "04000",
"card_holder_name": null,
"acquirer_mid": "***",
"payment_link_id": null,
"order_description": "21882 - SAR"
}
],
{
"data_count": 70
}
]
This is my root pojo and I parse it using string
#Data
#JsonInclude(JsonInclude.Include.NON_NULL)
public class DownloadReportResponse {
private TransactionCount transactionCount;
private List<TransactionsResponse> transactions;
}
Parsing :
List<DownloadReportResponse> properties = new ObjectMapper().readValue(report, new TypeReference<>() {
});
Expanding on my comment, you could try something like this:
ObjectMapper om = new ObjectMapper();
//read the json into a generic structure
JsonNode tree = om.readTree(json);
//check if the top level element is an array
if(tree.isArray()) {
//iterate over the elements
tree.forEach(element -> {
//distinguish between nested list and object
if(element.isArray()) {
List<TransactionsResponse> responses = om.convertValue(element, new TypeReference<List<TransactionsResponse>>(){});
//do whatever needed with the list
} else if(element.isObject()) {
TransactionCount txCount = om.convertValue(element, TransactionCount .class);
//use the count as needed
}
});
}
This depends on the knowledge that you get an array which contains an inner array of TransactionsResponse elements or objects of type TransactionCount but nothing else.
However, if you have a chance to modify the response I'd suggest you shoot for something like this which is way easier to parse and understand:
{
"transactions":[ ... ],
"transactionCount": {
"data_count": 70
}
}

Simplest way to convert this array in to the specified one using Java

I have this String Json Payload
[
"key1":{
"atr1":"key1",
"atr2":"value1",
"atr3":"value2",
"atr4":"value3,
"atr5":"value4"
},
"key2":{
"atr1":"key2",
"atr2":"value5",
"atr3":"value6",
"atr4":value7,
"atr5":"value8"
}
]
and I want it to be converted in to the following format using Java
[
{
"atr2":"value1",
"atr3":"value2",
"atr4":"value3,
"atr5":"value4"
},
{
"atr2":"value5",
"atr3":"value6",
"atr4": "value7",
"atr5":"value8"
}
]
What would be the simplest way of transforming this ?
You cannot, because the example below is not valid json.
Check it out using this JSON validator.
If you paste this in (I've fixed some basic errors with lack of quotes)
{
{
"atr2":"value1",
"atr3":"value2",
"atr4":"value3",
"atr5":"value4"
},
{
"atr2":"value5",
"atr3":"value6",
"atr4":"value7",
"atr5":"value8"
}
}
You will get these errors ...
It can work if you change the target schema to something like this by using a json-array to contain your data.
[
{
"atr2":"value1",
"atr3":"value2",
"atr4":"value3",
"atr5":"value4"
},
{
"atr2":"value5",
"atr3":"value6",
"atr4":"value7",
"atr5":"value8"
}
]
If this works for you, then this problem can easily be solved by using the ObjectMapper class.
You use it to deserealize the original JSON into a class, which has two fields "key1" and "key2"
Extract the values of these fields and then just store them in an array ...
Serialize the array using the ObjectMapper.
Here a link, which explains how to use the ObjectMapper class to achieve the goals above.
EDIT:
So you'll need the following classes to solve the problem ...
Stores the object data
class MyClass {
String atr2;
String art3;
}
Then you have a container class, which is used to store the initial json.
class MyClassContainer {
MyClass key1;
MyClass key2;
}
Here's how you do the parse from the original json to MyClassContainer
var mapper = new ObjectMapper()
var json = //Get the json String somehow
var myClassContainer = mapper.readValue(json,MyClassContainer.class)
var mc1 = myClassContainer.getKey1();
var mc2 = myClassContainer.getKey2();
var myArray = {key1, key2}
var resultJson = mapper.writeValueAsString(myArray)
Assuming that you will correct the JSON into a valid one (which involves replacing the surrounding square braces with curly ones, and correct enclosure of attribute values within quotes), here's a simpler way which involves only a few lines of core logic.
try{
ObjectMapper mapper = new ObjectMapper();
mapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
HashMap<String, Data> map = mapper.readValue( jsonString, new TypeReference<HashMap<String, Data>>(){} );
String json = mapper.writeValueAsString( map.values() );
System.out.println( json );
}
catch( JsonProcessingException e ){
e.printStackTrace();
}
jsonString above is your original JSON corrected and valid JSON input.
Also notice the setting of FAIL_ON_UNKNOWN_PROPERTIES to false to allow atr1 to be ignored while deserializing into Data.
Since we are completely throwing away attr1 and its value, the Data class will represent all fields apart from that.
private static class Data{
private String atr2;
private String atr3;
private String atr4;
private String atr5;
}

How to map a value in an nested array to a property

I am trying to parse a json and get hold of value inside an array -
{
"hits": {
"hits": [
{
"name": [
{
"family": "Doe",
"given": "Jon",
"middle": "Smith",
"use": "Commercial"
}
]
}
]
}
}
I realized that i have a one more array at root level so i decided to parse hits which is a list and then name which is another list. I am able to see entire name array in the response but instead i would rather want to see only given value that is mapped to fstNm
private String fstNm;
private List<Map<String,Object>> name;
public String getFstNm() { return fstNm; }
#JsonProperty("hits")
public void setFstNm(List<Map<String, Object>> hits) {
name = (List) hits.get(0).get("name");
this.fstNm= (String) name.get(0).get("given");
}
expected output-
{
"fstNm": "Jon"
}
I would appreciate any help here.
Just change the setFstNm() method as below
#JsonProperty("hits")
public void setFstNm(Map<String, List<Map<String, Object>>> hits) {
name = (List) hits.get("hits").get(0).get("name");
this.fstNm = (String) name.get(0).get("given");
}
Not sure why you have to have name be the same object type as hits
private List<Map<String,Object>> name;
I think you can accomplish getting all the values of name by declaring it as
`ArrayList<String> name;`
From there you can try to set name something like
ArrayList<String> name = (List) hits.get(0).get("name");
Then you can create a get method to get name and then access it in your setter like
ArrayList<String> name = getName();
fstNm = name.get(2);

Parse a URI String into a JSON object

I've got the URI like this:
http://localhost:8080/profile/55cbd?id=123&type=product&productCategories.id=ICTLicense&productCategories.name.firstName=Jack&productCategories.name.lastName=Sparrow&groups=a&groups=b
I need a JSON object like this:
{
"id": "123",
"type": "product",
"productCategories": {
"id": "ICTlicense",
"name": {
"firstName": "Jack",
"lastName": "Sparrow"
}
},
"groups":["a", "b"]
}
Query parameters nesting can be dynamic, like for example abc.def.ghi.jkl.mno=value1&abc.xyz=value2 will result in
{
"abc": {
"def": {
"ghi": {
"jkl": {
"mno": "value1"
}
}
},
"xyz": "value2"
}
}
I have tried this but it can not handle the nesting.
final Map<String, String> map = Splitter.on('&').trimResults().withKeyValueSeparator('=').split(request.getQuery());
How to do this in Java?
With the way that your URI string is structured it wouldn't be possible to nest it the way you'd like, here's why.
id=123 This is simple enough since id would just be an int
productCategories.id=ICTLicense This would also be simple enough since we can assume that productCategories is an object and id is a key inside of the object
However, it gets more complex when you start using arrays, for instance:
&groups=a&groups=b
How do you know that groups is an array, and not simply a key called groups with a value of a or b
Also, you store all your data to Map<String, String>,
This wouldn't support arrays as it stores objects to key-value, so you wouldn't be able to have multiple keys of groups with different values.
I'd also suggest you use a library like Gson and parse your data to a JsonObject
https://github.com/google/gson
If you were to use Gson, you could do something similar to this:
public JsonObject convertToJson(String urlString) {
//Create a JsonObject to store all our data to
JsonObject json = new JsonObject();
//Split the data part of the url by the props
String[] props = urlString.split("&");
//Loop through every prop in the url
for (String prop : props) {
//Create a list of all the props and nested props
String[] nestedProps = prop.split("=")[0].split("\\.");
//Get the actual key for our prop
String key = nestedProps[nestedProps.length - 1];
//Get the value
String value = prop.split("=")[1];
//Loop through our props array
for (String nestedProp : nestedProps) {
//If the property already exists, then skip
if (json.has(nestedProp)) continue;
//If the prop is the key, add it to the json object
if(nestedProp.equalsIgnoreCase(key)) {
json.addProperty(nestedProp, value);
continue;
}
//If the above checks fail, then create an object in the json
json.add(nestedProp, new JsonObject());
}
}
return json;
}

How to rename key in JSONObject using java?

I want to rename the keys of a JSON object using Java.
My input JSON is:
{
"serviceCentreLon":73.003742,
"type":"servicecentre",
"serviceCentreLat":19.121737,
"clientId":"NMMC01"
}
I want to change it to:
{
"longitude":73.003742,
"type":"servicecentre",
"latitude":19.121737,
"clientId":"NMMC01"
}
i.e. I want to rename "serviceCentreLon" to "longitude" and "serviceCentreLat" to "latitude". I am using the JSONObject type in my code.
Assuming you're using the json.org library: once you have a JSONObject, why not just do this?
obj.put("longitude", obj.get("serviceCentreLon"));
obj.remove("serviceCentreLon");
obj.put("latitude", obj.get("serviceCentreLat"));
obj.remove("serviceCentreLat");
You could create a rename method that does this (then call it twice), but that's probably overkill if these are the only fields you're renaming.
String data= json.toString();
data=data.replace("serviceCentreLon","longitude");
data=data.replace("serviceCentreLat","latitude");
convert back to json object
I'm not sure whether I get your question right, but shouldn't the following work?
You could use a regular expression to replace the keys, for example:
String str = myJsonObject.toString();
str = str.replace(/"serviceCentreLon":/g, '"longitude":');
str = str.replace(/"serviceCentreLat":/g, '"latitude":');
It's not as "clean", but it might get the job done fast.
To build on Danyal Sandeelo's approach, instead of:
data=data.replace("serviceCentreLon","longitude");
use
data=data.replace("\"serviceCentreLon\":","\"longitude\":");
This method explicitly matches the json key syntax, and avoids obscure errors where the key value is present as valid data elsewhere in the json string.
The best way to approach the problem is to parse the JSON data and then replace the key. A number of parsers are available - google gson, Jackson serializer de-serializers, org.json.me are a few such java libraries to handle JSON data.
http://www.mkyong.com/java/how-do-convert-java-object-to-from-json-format-gson-api/
is a good way to deal with it if you have a pretty generic and relatively huge JSON data. Of course, you have to spend time in learning the library and how to use it well.
http://www.baeldung.com/jackson-map is another such parser.
https://stleary.github.io/JSON-java/ is the simplest one especially if you don't want any serious serialization or deserialization
Have an object that maps to this object data structure.
Use GSON parser or Jackson parser to convert this json into POJO.
Then map this object to another Java Object with required configuration
Convert that POJO back to json using the same GSON parsers.
refer this for further reference
http://www.mkyong.com/java/how-do-convert-java-object-to-from-json-format-gson-api/
I faced this problem during my work so I've made a useful Utils class and I want to share it with you.
package net.so.json;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.json.JSONObject;
public class Utils {
/**
* replace json object keys with the new one
* #param jsonString represents json object as string
* #param oldJsonKeyNewJsonKeyMap map its key old json key & its value new key name if nested json key you have traverse
* through it using . example (root.coutry, count) "root.country" means country is a key inside root object
* and "count" is the new name for country key
* Also, if the value for the map key is null, this key will be removed from json
*/
public static void replaceJsonKeys(final JSONObject jsonObject, final Map<String, String> oldJsonKeyNewJsonKeyMap) {
if (null == jsonObject || null == oldJsonKeyNewJsonKeyMap) {
return;
}
// sort the old json keys descending because we want to replace the name of the inner most key first, then
// the outer one
final List<String> oldJsonKeys = oldJsonKeyNewJsonKeyMap.keySet().stream().sorted((k2, k1) -> k1.compareTo(k2)).collect(Collectors.toList());
oldJsonKeys.forEach(k -> {
// split old key, remember old key is something like than root.country
final String[] oldJsonKeyArr = k.split("\\.");
final int N = oldJsonKeyArr.length;
// get the object hold that old key
JSONObject tempJsonObject = jsonObject;
for (int i = 0; i < N - 1; i++)
tempJsonObject = tempJsonObject.getJSONObject(oldJsonKeyArr[i]);
final String newJsonKey = oldJsonKeyNewJsonKeyMap.get(k);
// if value of the map for a give old json key is null, we just remove that key from json object
if (!"null".equalsIgnoreCase(newJsonKey))
tempJsonObject.put(newJsonKey, tempJsonObject.get(oldJsonKeyArr[N - 1]));
// remove the old json key
tempJsonObject.remove(oldJsonKeyArr[N - 1]);
});
}
}
you can test this class by running App
package net.so.json;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;
public class App {
public static void main(String[] args) {
final String jsonString = "{\"root\":{\"country\": \"test-country\", \"city\": \"test-city\"}}";
final JSONObject jsonObject = new JSONObject(jsonString);
System.out.println("json before replacement: " + jsonObject);
/* will get >>
{
"root": {
"country": "test-country",
"city": "test-city"
}
}
*/
// construct map of key replacements
final Map<String, String> map = new HashMap<>();
map.put("root", "root2");
map.put("root.country", "count");
map.put("root.city", "null"); // null as a value means we want to remove this key
Utils.replaceJsonKeys(jsonObject, map);
System.out.println("json after replacement: " + jsonObject);
/* will get >>
{
"root2": {
"count": "test-country"
}
}
*/
}
}
I ran into a scenario where I wanted to remove a hyphen from an unknown number of keys in a nested object.
So this:
{
"-frame": {
"-shape": {
"-rectangle": {
"-version": "1"
}
},
"-path": {
"-geometry": {
"-start": {
"-x": "26.883513064453602",
"-y": "31.986310940359715"
}
},
"-id": 1,
"-type": "dribble",
"-name": "MultiSegmentStencil",
"-arrowhead": "0"
}
}
}
Would be this:
{
"frame": {
"shape": {
"rectangle": {
"version": "1"
}
},
"path": {
"geometry": {
"start": {
"x": "26.883513064453602",
"y": "31.986310940359715"
}
},
"id": 1,
"type": "dribble",
"name": "MultiSegmentStencil",
"arrowhead": "0"
}
}
}
A recursive method(kotlin).. with a list did the trick via Jackson
fun normalizeKeys(tree: JsonNode, fieldsToBeRemoved: MutableList<String>) {
val node = tree as ContainerNode<*>
val firstClassFields = node.fields()
while(firstClassFields.hasNext()) {
val field = firstClassFields.next()
if(field.key.substring(0,1) == "-") {
fieldsToBeRemoved.add(field.key)
}
if(field.value.isContainerNode) {
normalizeKeys(field.value, fieldsToBeRemoved)
}
}
fieldsToBeRemoved.forEach {
val fieldByKey: MutableMap.MutableEntry<String, JsonNode>? = getFieldByKey(tree, it)
if(fieldByKey != null) {
(tree as ObjectNode)[fieldByKey!!.key.replaceFirst("-","")] = fieldByKey.value
(tree as ObjectNode).remove(fieldByKey!!.key)
}
}
}

Categories