0

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.

User27854
  • 824
  • 1
  • 16
  • 40
  • Check [Java Validation Frameworks](https://stackoverflow.com/q/397852/10819573) – Arvind Kumar Avinash Jan 11 '23 at 20:24
  • @ArvindKumarAvinash these are for field level validation right. I want to rewrite the validation for the individual json values. – User27854 Jan 12 '23 at 10:05
  • 1
    Is there any reason you don't create custom class representing the json, deserialize the json into this class and use existing validation framework to validate the deserialized object? Currently you are doing essentially the same, but in a more awkward way with a `Map`. Even if you insist on manual validation, working with a custom class will be much less clunky compared to a map. – Chaosfire Jan 20 '23 at 15:57
  • @Chaosfire, the response has lot of dynamic fields so converting it into a class is not a good idea for my actual scenario.. – User27854 Jan 20 '23 at 16:38
  • @Chaosfire Out of 40+ fields only 15 or odd fields needs validations and other does not need. – User27854 Jan 20 '23 at 16:44
  • @User27854 You can always make a class only with the fields you actually need to validate and not declare the rest. But more importantly, since a lot of them are dynamic, how do you know which fields to validate? When you receive the json, how do you know that you should validate properties `a` and `b`, but not `c` and `d`? Same question about properties in nested objects, how do you know you should validate `offerSpecifications.price` and not `offerSpecifications.another` for example? – Chaosfire Jan 20 '23 at 17:50

3 Answers3

2

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;
        }
    }
}
Raymond Choi
  • 1,065
  • 2
  • 7
  • 8
1

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

Lae
  • 589
  • 1
  • 5
0

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 :

  1. A empty constructor
  2. An all arguments constructor
  3. Three private properties, Name, Price, InStock
  4. 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.

  • As mentioned in the problem statement, the Json response will have a minimum of 40+ fields and also they are dynamic. That few values may be added or few removed. When we have so many fields, creating a class does will be difficult. – User27854 Jan 20 '23 at 16:43
  • Out of 40+ fields only 15 or odd fields needs validations and other does not need. – User27854 Jan 20 '23 at 16:43
  • In your program if you are trying to obtain a map of json string. I am already doing that. – User27854 Jan 20 '23 at 16:46
  • Okay, what are the fields 15 that will be validated ? I am assuming it will be mandatory for them to exist with a value within the JSON right? – Samir Zafar Jan 20 '23 at 17:01
  • yes those 15 fields requires Validation, but they are not the same for every field ed: product field must not be empty, where as for offerAttributes it must not be empty and must have one or more json object, Also for offerStartDate it must not be empty and must of a valid date. – User27854 Jan 20 '23 at 17:28
  • These can be addressed by design pattern, But i am not sure as to which will be apt for my case. – User27854 Jan 20 '23 at 17:29
  • An alternative would be to make a class with the 15 mandatory fields. Then based on the field types validate with the specific logic. I am not sure which Design Patterns fit this specific use case but I can't see why the suggested solution is not applicable. – Samir Zafar Jan 20 '23 at 17:32