I have a hashmap which has key value pair of String and object. It is the conversion of something like below json.
{
"test1": {
"test2": {
"test3": {
"key": "value"
},
"somefields12": "some value2"
},
"somefields": "some value"
}
}
But, I am not converting to map. I have just that map. If this may has key and value , I have to do write some logic based on that value. I implemented as below:
if (map.containsKey("test1") ) {
final HashMap<String, Object> test1 = (HashMap<String, Object>) map.get("test1");
if (test1.containsKey("test2")) {
final List<Object> test2 = (List<Object>) test1.get("test2");
if (!test2.isEmpty()) {
final HashMap<String, Object> test3 = (HashMap<String, Object>) test2.get(0);
if (test3.containsKey("key")) {
final String value = String.valueOf(test2.get("key"));
if (!StringUtils.isBlank(value)) {
//do some work based on value
}
}
}
}
}
Now, I wanted to avoid the nested if (multiple ifs) from my code. What would be the best way to do so?
I'm not familiar with the fancy new Java 8 features, so I'd do it the old fashioned way with a function that takes a path to look up, and walks the list with a loop:
import java.util.*;
class Test {
static String getByPath(HashMap<String, Object> map, String... path) {
for(int i=0; i<path.length-1; i++) {
map = (HashMap<String, Object>) map.get(path[i]);
if (map == null) return null;
}
Object value = map.get(path[path.length-1]);
return value == null ? null : String.valueOf(value);
}
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
HashMap<String, Object> tmp1 = new HashMap<>();
HashMap<String, Object> tmp2 = new HashMap<>();
map.put("test1", tmp1);
tmp1.put("test2", tmp2);
tmp2.put("key1", "My Value");
System.out.println("With valid path: " + getByPath(map, "test1", "test2", "key1"));
System.out.println("With invalid path: " + getByPath(map, "test1", "BANANA", "key1"));
}
}
This results in:
With valid path: My Value
With invalid path: null
This can optionally be extended to:
Check that nodes are in fact maps before casting
Use Optional or a helpful exception instead of returning null
Using Gson library combined with java 8 Optional you can do something like this:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Optional;
public class FindJsonKey {
public static final String JSON = "{\n" +
" \"test1\": {\n" +
" \"test2\": {\n" +
" \"test3\": {\n" +
" \"key\": \"value\"\n" +
" },\n" +
" \"somefields12\": \"some value2\"\n" +
" },\n" +
" \"somefields\": \"some value\"\n" +
" }\n" +
"}";
public static void main(String[] args) {
Gson gson = new GsonBuilder().create();
JsonObject jsonObject = gson.fromJson(JSON, JsonObject.class);
Optional
.ofNullable(jsonObject.getAsJsonObject("test1"))
.map(test1 -> test1.getAsJsonObject("test2"))
.map(test2 -> test2.getAsJsonObject("test3"))
.map(test3 -> test3.get("key"))
.map(JsonElement::getAsString)
.ifPresent(key -> {
// Do something with key..
});
}
}
A recursive solution that assumes the JSON structure is as given -- and ignores the names of the nested parent maps -- could look like the following:
/**
* Traverses the map from {#code nextEntry} until a key matching {#code keyTofind} is found
* or the bottom of the map is reached.
* The method assumes that any nested maps are contained in the first
* element of the parent map.
*
* #param nextEntry the next entry to search from
* #param keyToFind the stopping condition for the recursion
* #return the value matching {#code keyToFind} (may be empty)
*/
private static Optional<Object> find(Map.Entry<String,Object> nextEntry, String keyToFind) {
Object value = nextEntry.getValue();
if (nextEntry.getKey().equals(keyToFind)) {
return Optional.of(value);
} else if (nextEntry.getValue() instanceof Map) {
return find(((Map<String, Object>)nextEntry.getValue())
.entrySet().iterator().next(), keyToFind);
} else {
return Optional.empty();
}
}
Testing with the provided data (with help of Jackson for JSON parsing)
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Test {
private static final String JSON = "{ "
+ "\"test1\": {"
+ " \"test2\": {"
+ " \"test3\": {"
+ " \"key\": \"value\""
+ "},"
+ " \"somefields12\": \"some value2\""
+ "},"
+ " \"somefields\": \"some value\""
+ "}"
+ "}";
//...
public static void main(String[] args) throws Exception {
Map<String, Object> json = new ObjectMapper()
.readValue(JSON, new TypeReference<Map<String, Object>>() { });
final String keyToFind = "key";
Optional<Object> value = find(json.entrySet().iterator().next(), keyToFind);
System.out.println(value.isPresent() ? value.get() : "null");
}
}
gives
value
Related
I have a JSON String:
{
"productName": "Gold",
"offerStartDate": "01152023",
"offerEndDate": "01152024",
"offerAttributes": [
{
"id": "TGH-DAD3KVF3",
"storeid": "STG-67925",
"availability": true
}
],
"offerSpecifications": {
"price": 23.25
}
}
The validation logic for the same is written as
Map<String, Object> map = mapper.readValue(json, Map.class);
String productNameValue = (String)map.get("productName");
if(productNameValue ==null && productNameValue.isEmpty()) {
throw new Exception();
}
String offerStartDateValue = (String)map.get("offerStartDate");
if(offerStartDateValue ==null && offerStartDateValue.isEmpty()) {
throw new Exception();
}
List<Object> offerAttributesValue = (List)map.get("offerAttributes");
if(offerAttributesValue ==null && offerAttributesValue.isEmpty()) {
throw new Exception();
}
Map<String, Object> offerSpecificationsValue = (Map)map.get("offerSpecifications");
if(offerSpecificationsValue ==null && offerSpecificationsValue.isEmpty() && ((String)offerSpecificationsValue.get("price")).isEmpty()) {
throw new Exception();
}
This is a portion of the JSON response. The actual response has more than 48 fields and the. The existing code has validation implemented for all the 48 fields as above. Note the responses are way complex than these.
I feel the validation code has very verbose and is very repetitive. How do, I fix this? What design pattern should I use to write a validation logic. I have seen builder pattern, but not sure how to use it for this scenario.
Build a JSON template for comparison.
ObjectMapper mapper = new ObjectMapper();
JsonNode template = mapper.readTree(
"{" +
" \"productName\": \"\"," +
" \"offerStartDate\": \"\"," +
" \"offerEndDate\": \"\"," +
" \"offerAttributes\": []," +
" \"offerSpecifications\": {" +
" \"price\": 0" +
" }" +
"}");
JsonNode data = mapper.readTree(
"{" +
" \"productName\": \"Gold\"," +
" \"offerStartDate\": \"01152023\"," +
" \"offerEndDate\": \"01152024\"," +
" \"offerAttributes\": [" +
" {" +
" \"id\": \"TGH-DAD3KVF3\"," +
" \"storeid\": \"STG-67925\"," +
" \"availability\": true" +
" }" +
" ]," +
" \"offerSpecifications\": {" +
" \"price\": 23.25" +
" }" +
"}");
validate(template, data);
Here is the recursion function to compare the template and the data.
public void validate(JsonNode template, JsonNode data) throws Exception {
final Iterator<Map.Entry<String, JsonNode>> iterator = template.fields();
while (iterator.hasNext()) {
final Map.Entry<String, JsonNode> entry = iterator.next();
JsonNode dataValue = data.get(entry.getKey());
if (dataValue == null || dataValue.isNull()) {
throw new Exception("Missing " + entry.getKey());
}
if (entry.getValue().getNodeType() != dataValue.getNodeType()) {
throw new Exception("Mismatch data type: " + entry.getKey());
}
switch (entry.getValue().getNodeType()) {
case STRING:
if (dataValue.asText().isEmpty()) {
throw new Exception("Missing " + entry.getKey());
}
break;
case OBJECT:
validate(entry.getValue(), dataValue);
break;
case ARRAY:
if (dataValue.isEmpty()) {
throw new Exception("Array " + entry.getKey() + " must not be empty");
}
break;
}
}
}
Option 1
If you are able to deserialize into a class instead of a map, you can use Bean Validaion to do something like this:
class Product {
#NotEmpty String productName;
#NotEmpty String offerStartDate;
#NotEmpty String offerEndDate;
}
You can then write your own #MyCustomValidation for any custom validation.
Option 2
If you must keep the object as a Map, you can extract each of your validations into validator objects make things extensible/composable/cleaner, roughly like this:
#FunctionalInterface
interface Constraint {
void validate(Object value);
default Constraint and(Constraint next) {
return (value) -> {
this.validate(value);
next.validate(value);
};
}
}
var constraints = new HashMap<Validator>();
constraints.put("productName", new NotEmpty());
constraints.put("offerStartDate", new NotEmpty());
constraints.put("someNumber", new LessThan(10).and(new GreaterThan(5)));
// Then
map.forEach((key, value) -> {
var constraint = constraints.get(key);
if (constraint != null) {
constraint.validate(value);
}
});
Hello, you can validate JSON with the following approach.
I created a mini, scaled-down version of yours but it should point you towards the right direction hopefully.
I used the Jackson libary, specifically I used the object mapper.
Here is some documentation to the object mapper - https://www.baeldung.com/jackson-object-mapper-tutorial.
So I created a simple Product class.
The Product class was composed of :
A empty constructor
An all arguments constructor
Three private properties, Name, Price, InStock
Getters & Setters
Here is my Main.java code.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
// Mock JSON
String json = """
{
"name" : "",
"price" : 5.99,
"inStock": true
}""";
// Declare & Initialise a new object mapper
ObjectMapper mapper = new ObjectMapper();
// Declare & Initialise a new product object
Product product = null;
try {
// Try reading the values from the json into product object
product = mapper.readValue(json, Product.class);
}
catch (Exception exception) {
System.out.println("Exception");
System.out.println(exception.getMessage());
return;
}
// Map out the object's field names to the field values
Map<String, Object> propertiesOfProductObject = mapper.convertValue(product, Map.class);
// Loop through all the entries in the map. i.e. the fields and their values
for(Map.Entry<String, Object> entry : propertiesOfProductObject.entrySet()){
// If a value is empty throw an exception
// Else print it to the console
if (entry.getValue().toString().isEmpty()) {
throw new Exception("Missing Attribute : " + entry.getKey());
} else {
System.out.println(entry.getKey() + "-->" + entry.getValue());
}
}
}
}
It throws an error sauing the name field is empty.
I have JSON-object which has a dynamic key inside it. I need to get a specific value mapped to this dynamic Key.
For example: value "10.00" will be returned for the key "value" and value REFUND_COMPLETED will be obtained as a result for the key "refundState"
Code:
public static void main(String[] args) {
String json2 = "{\n"
+ " \"refundStatusDetails\": {\n"
+ " \"txn_f2a7802c-ef84-43c3-8615-5f706b995c23\": {\n"
+ " \"refundTransactionId\": \"txn_f2a7802c-ef84-43c3-8615-5f706b995c23\",\n"
+ " \"requestId\": \"refund-request-id-1\",\n"
+ " \"refundState\": \"REFUND_COMPLETED\",\n"
+ " \"amount\": {\n"
+ " \"currency\": \"INR\",\n"
+ " \"value\": \"10.00\"\n"
+ " },\n"
+ " \"refundRequestedTime\": 1513788119505,\n"
+ "}";
System.out.println("JSON: " + json2);
JsonParser p = new JsonParser();
Map<String,String> res =check("refundState", p.parse(json2));
System.out.println("JSON: " + res.get("refundState"));
}
private static Map<String,String> check(String key, JsonElement jsonElement) {
Map<String,String> res = new HashMap<>();
if (jsonElement.isJsonObject()) {
Set<Map.Entry<String, JsonElement>> entrySet = jsonElement.getAsJsonObject().entrySet();
entrySet.stream().forEach((x) ->{
if (x.getKey().equals(key)) {
res.put(x.getKey(),x.getValue().toString());
}
});
}
return res;
}
If you are interested to access any field in a JSON object structure then you can use a method like the following one that I used to access the fields that I needed from an JSON Array structure using "package org.json;"
public static final String COLLECTION_OBJECT = "collectionObject";
public static final String FIELD = "field";
private ArrayList<Object> getSearchFilterCriteriaAsString() {
String jsonString = "{" +
"\n\"collectionObject\": " +
"[\n" +
"{" +
"\n\"field\": \"productId\"," +
"\n\"value\": \"3\"," +
"\n\"operator\": \"EQUALS\"\n" +
"},\n" +
"{" +
"\n\"field\": \"productPrice\"," +
"\n\"value\": \"15\"," +
"\n\"operator\": \"MORE_THAN\"\n" +
"},\n" +
"{" +
"\n\"field\": \"productQuantity\"," +
"\n\"value\": \"25\"," +
"\n\"operator\": \"LESS_THAN\"\n" +
"}\n" +
"]\n" +
"}";
JSONObject jsonObject = new JSONObject(jsonString);
JSONArray jsonArray = jsonObject.getJSONArray(COLLECTION_OBJECT);
ArrayList<Object> filteredObjectsList = new ArrayList<>();
if (jsonArray != null) {
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject filteredObj = (JSONObject) jsonArray.get(i);
filteredObjectsList.add(filteredObj.getString(FIELD));
}
}
return filteredObjectsList;
}
As long as you know your key values you can parse any JSON as deep you want, without to care about how big it is, how many attributes it has.
I have JSON Object which has a dynamic key inside the object I need a
specific key value
The recursive method listed below is capable of fetching the value mapped to the provided key from a nested JSON-object.
The method return an optional result.
If provided JsonElement is not a JsonObject an empty optional will be returned.
Otherwise, if the given JSON-object contains the given key the corresponding value wrapped by an optional will be returned. Or if it's not the case the entry set obtained from an object will be processed with stream. And for every JSON-object in the stream method getValue() will be called recursively.
If the given key is present in one of the nested objects, the first encountered non-empty optional will be returned. Or empty optional if the key was not found.
private static Optional<JsonElement> getValue(String key, JsonElement jsonElement) {
if (!jsonElement.isJsonObject()) {
return Optional.empty();
}
JsonObject source = jsonElement.getAsJsonObject();
return source.has(key) ? Optional.of(source.get(key)) :
source.entrySet().stream()
.map(Map.Entry::getValue)
.filter(JsonElement::isJsonObject)
.map(element -> getValue(key, element))
.filter(Optional::isPresent)
.findFirst()
.orElse(Optional.empty());
}
main() - demo
public static void main(String[] args) {
String json2 =
"""
{
"refundStatusDetails": {
"txn_f2a7802c-ef84-43c3-8615-5f706b995c23": {
"refundTransactionId": "txn_f2a7802c-ef84-43c3-8615-5f706b995c23",
"requestId": "refund-request-id-1",
"refundState": "REFUND_COMPLETED",
"amount": {
"currency": "INR",
"value": "10.00"
},
"refundRequestedTime": "1513788119505"
}
}
}""";
JsonElement element = JsonParser.parseString(json2);
System.out.println(getValue("refundState", element));
System.out.println(getValue("value", element));
}
Output
Optional["REFUND_COMPLETED"]
Optional["10.00"]
Note:
If you are using Java 17 you can utilize text blocks by plasing the text between the triple quotation marks """ JSON """.
Constructor of the JsonParser is deprecated. Instead of instantiating this class, we have to use its static methods.
I have an ObjectNode, that looks as follows
{
"Header":{
"sub-header1":{
"#field":"value",
"#field":"value",
},
"sub-header2":{
"field":"",
"field":"",
"field":"",
"panel_field":{
"value":"",
"value":""
}
}
}
Now, what I want to do is to get all the fields from sub-headers in a list. This is the method that I'm employing
public static List<String> getDocumentFields(ObjectNode jsonDocumentNode) {
List<String> documentFields = new ArrayList<>();
Iterator<JsonNode> fields = jsonDocumentNode.elements();
while (fields.hasNext()) {
JsonNode jsonNode = fields.next();
Iterator<Map.Entry<String, JsonNode>> jsonFields = jsonNode.fields();
while (jsonFields.hasNext()) {
Map.Entry<String, JsonNode> jsonNodeEntry = jsonFields.next();
documentFields.add(jsonNodeEntry.getKey());
}
}
return documentFields;
}
But, I'm only getting headers in the list like {sub-header1, sub-header2}, instead of fields. How can I fix this? I'd really appreciate any kind of help.
EDIT:
While #sfiss's answer helped a great deal, I still wanted to find a way to do so without hardcoding the loop-logic and this answer turned out to be the exact thing I was looking for.
Well, it's simple, you are not iterating deep enough (field list is on third level). If you know the structure of your JSON, just iterate until you find the desired fields:
public class MyTest {
#Test
public void testJson() throws IOException {
final String json = getJson();
final JsonNode jsonDocumentNode = new ObjectMapper().readTree(json);
final List<String> fields = getDocumentFields((ObjectNode) jsonDocumentNode);
assertThat(fields, Matchers.contains("#field1", "#field2", "field1", "field2", "field3", "panel_field"));
}
public static String getJson() {
return "{\r\n" +
" \"Header\":{\r\n" +
" \"sub-header1\":{\r\n" +
" \"#field1\":\"value\",\r\n" +
" \"#field2\":\"value\"\r\n" +
" },\r\n" +
" \"sub-header2\":{\r\n" +
" \"field1\":\"\",\r\n" +
" \"field2\":\"\",\r\n" +
" \"field3\":\"\",\r\n" +
" \"panel_field\":{\r\n" +
" \"value1\":\"\",\r\n" +
" \"value2\":\"\"\r\n" +
" }\r\n" +
" } \r\n" +
" }\r\n" +
"}";
}
public static List<String> getDocumentFields(final ObjectNode jsonDocumentNode) {
final List<String> documentFields = new ArrayList<>();
for (final JsonNode header : (Iterable<JsonNode>) jsonDocumentNode::elements) {
for (final JsonNode subheader : (Iterable<JsonNode>) header::elements) {
for (final Map.Entry<String, JsonNode> field : (Iterable<Map.Entry<String, JsonNode>>) subheader::fields) {
documentFields.add(field.getKey());
}
}
}
return documentFields;
}
}
However, I would argue that it is simpler to let jackson serialize the JSON into a convenient data structure, and you just use the POJO's getters to obtain your values. That would also make it more clear than handling the JsonNode.
Just FYI, I edited your JSON slightly and used Java 8 SAM conversions to create iterables for the foreach-loops from the iterators, but you can still use your code and iterate one more level using while and iterators.
I have the following json file
[{
"en": {
"key1": "Ap",
"key2": "ap2"
}
},
{
"ar": {
"key1": "Ap",
"key2": "ap2"
}
}
]
I would like to create a Map in Java such as the key is the language (like en or ar) and the value is a object. Something like this.
public class Category {
private String key1;
private String key2;
}
Type type = new TypeToken<Map<String, Category>>() {}.getType();
Gson gson = new Gson();
InputStream in = MyClass.class.getResourceAsStream("/categories.json");
String text = IOUtils.toString(in, StandardCharsets.UTF_8);
Map<String, Category> map = gson.fromJson(text, type);
But when I run this code, I get errors:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 3 path $[0]
Is my Json structure wrong or is there an easier way to map this?
try to read this json file
{
"ar": {
"key1": "Ap",
"key2": "ap2"
},
"en": {
"key1": "Ap",
"key2": "ap2"
}
}
The above json is collection of JsonObject like list or array, so just parse it to List of Map objects
Type type = new TypeToken<List<Map<String, Category>>>() {}.getType();
Your json is a list of maps, not only maps. So you have to add it to the type declared.
Try this:
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
public class Category {
private String key1;
private String key2;
#Override
public String toString() {
return "Category{" +
"key1='" + key1 + '\'' +
", key2='" + key2 + '\'' +
'}';
}
public static void main(String[] args) {
String json = "[{\n" +
" \"en\": {\n" +
" \"key1\": \"Ap\",\n" +
" \"key2\": \"ap2\"\n" +
" }\n" +
" },\n" +
"\n" +
" {\n" +
" \"ar\": {\n" +
" \"key1\": \"Ap\",\n" +
" \"key2\": \"ap2\"\n" +
" }\n" +
" }\n" +
"]";
Type type = new TypeToken<List<Map<String, Category>>>() {}.getType();
Gson gson = new Gson();
List<Map<String, Category>> maps = gson.fromJson(json, type);
System.out.println(maps);
}
}
Your input is an json array with two objects. However your target variable 'type' is a of type Object and not an 'Array of Objects'. In simpler terms, Map cannot store an
Array.
Lets take a simpler approach to this problem(not a recommended approach). If we convert the map manually to an array of maps, that would look like this:
yourJson -> [Map1(en,category1(Ap,ap2)),Map2(en,category2(Ap,ap2))]
i.e. An array of Maps
So in java equivalent this becomes:
Type typeOfT2 = new TypeToken<ArrayList<HashMap<String, Category>>>() {}.getType();
List<HashMap<String, Category>> list = gson.fromJson(text, typeOfT2);
We get to what we want, but there are better ways of doing this. We need Jackson instead of Gson for this.(Some one may add a Gson based solution, pretty sure a cleaner one than above exists). Here we will use ObjectMapper from com.fasterxml.jackson.databind.ObjectMapper
ObjectMapper om = new ObjectMapper();
List<Map.Entry<String, Category>> listx = om.readValue(text, ArrayList.class);
If you print listx. You can see this(overridden toString() of Category class):
[{en={key1=Ap, key2=ap2}}, {ar={key1=Ap, key2=ap2}}]
listx is the most accurate representation of your json and not a Map.
Now if you need a map, I will leave that as an exercise for you about how to convert your List of Map.Entry to a Map implementation.
PS.: First long answer here. Apologies for any mistakes.
I am having some trouble deserializing the following JSON into a POJO. I have no control over the JSON structure, else I would've implemented it in some other way, but, that's life for you.
{
"1":{
"test":"1",
"other":"stuff"
},
"2":{
"test":"2",
"other":"stuff2"
}
}
Anyway, I am trying to deserialize by using a POJO with:
public Map<Integer, Payload> payload;
but although the Map does have a size of 2, when I try to get each of it, it's contents are null. Any idea on what I am doing wrong?
Thank you
I have no idea how the payload class looks like, but it should be something like this:
class Payload {
String test;
String other;
#Override
public String toString() {
return "Payload [test=" + test + ", other=" + other + "]";
}
}
If you assert this condition, then you can deserialize the json using a TypeToken> as token as danypata suggest... like:
public static void main(String args[]) throws InterruptedException {
String ff = "{\"1\":{" + "\"test\":\"1\"," + "\"other\":\"stuff\"" + "}," + "\"2\":{" + "\"test\":\"2\","
+ "\"other\":\"stuff2\"" + "}}";
Gson gson = new Gson();
Type mapType = new TypeToken<Map<String, Payload>>() {
}.getType();
Map<String, Payload> map = gson.fromJson(ff, mapType);
System.out.println(map);
for (Entry<String, Payload> entry : map.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
}
giving as result:
{1=Payload [test=1, other=stuff], 2=Payload [test=2, other=stuff2]}
1
Payload [test=1, other=stuff]
2
Payload [test=2, other=stuff2]
Why don't you use the Android JSONObject class? Then you can parse with it your entire JSON string and then you can obtain the values easily. For example this is to get the values of your "1" JSON object:
final JSONObject jsonObject = new JSONObject(jsonString);
final String test = jsonObject.getJSONObject("1").getString("test");
final String other = jsonObject.getJSONObject("1").getString("other");