2

I'm trying to read data for an objects from a JSON file. I want to make my function read objects one by one, instead of as an array of objects (so i can handle exceptions for each object), so i have to make a regex, which splits each JSON object's data string. The thing is, each object contains another object in it, so i can't make the regex split at "}, {", because it will split at the inner curly braces, not the outer ones. Here is an example JSON file:

{ 
  "vinForRepair" : "ABCDE123455432199",
  "dateOfRepair" : "17/08/2021",
  "mileage" : 100000,
  "items" : [ {
    "description" : "Water pump",
    "quantity" : 1,
    "price" : 120.0,
    "metric" : "UNIT
}, {         <---------This should be ignored
    "description" : "Motor oil",
    "quantity" : 1,
    "price" : 30.0,
    "metric" : "LITER"
  } ]
}, {         <---------This is where i want to split
  "vinForRepair" : "ABCDE123455432100",
  "dateOfRepair" : "15/08/2021",
  "mileage" : 250000,
  "items" : [ {
    "description" : "Break fluid",
    "quantity" : 1,
    "price" : 20.0,
    "metric" : "LITER"
  }, {       <---------This should be ignored
    "description" : "Tyre",
    "quantity" : 2,
    "price" : 80.0,
    "metric" : "UNIT"
 } ]
}

I tried to use

String[] jsonObjectStringArray = jsonString.split("\\}, \\{\\n  \"vinForRepair\"");

to split at the first object property, but it doesn't work.

I tried to use Jackson for this, but it either reads only one object, or an array of objects, and as i said i don't want to read an array directly from the file.

2
  • "make my function read objects one by one, instead of as an array of objects (so i can handle exceptions for each object)" why cant you just read the array of objects, and then for each item in the array, you pass the item to your function one by one? Commented Aug 17, 2021 at 13:31
  • @deecue because if only one object has an invalid property name in the JSON file, then the whole array will not be created Commented Aug 17, 2021 at 13:48

2 Answers 2

2

Instead of using regex you can do that by counting "depth" of json. When '{' occures depth is increased and otherway, when '}' occures depth is decreased. Braces at depth zero are places where you want to split.

private static List<String> split(String json) {
    List<String> result = new ArrayList<>();
    int depth = 0;
    int start = 0;
    for (int i = 0; i < json.length(); i++) {
        if (json.charAt(i) == '{') {
            if (depth == 0) {
                start = i;
            }
            depth++;
        }
        if (json.charAt(i) == '}') {
            depth--;
            if (depth == 0) {
                result.add(json.substring(start, i + 1));
            }
        }
    }
    return result;
}

You can run that method here https://ideone.com/vmnmCs

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much! After i get the JSON objects individually, now i can iterate over them and check if every one can be cast to the correct class i need. Simple and effective, thank you again!
0

I'm not really in favour of using regex for marshalling/unmarshalling data in established transfer formats - that is what Jackson excels at. I would leave regex for very specific capturing scenarios, as it is notoriously difficult to maintain. I manage to cook a solution in a few minutes and should work straight out of the box with Java 11+ (provided you have the customary dependencies on apache utils, jackson, hamcrest and logback):

EDIT: My response may be already too late, but I'm standing with my decision to not implement the parsing algorithm or use regex, but rather delegate to Jackson. I've changed my example to provide more resilient handling through custom deserialization, where you can decide what to do if malformed data is encountered. Maybe it's helpful to others.

public class MyJsonDeserializer {

    static final Logger logger = LoggerFactory.getLogger(MyJsonDeserializer.class);

    private static String payload = "{ \n" +
            "  \"vinForRepair\" : \"ABCDE123455432199\",\n" +
            "  \"dateOfRepair\" : \"17/08/2021\",\n" +
            "  \"mileage\" : 100000,\n" +
            "  \"items\" : [ {\n" +
            "    \"description\" : \"Water pump\",\n" +
            "    \"quantity\" : 1,\n" +
            "    \"price\" : 120.0,\n" +
            "    \"metric\" : \"UNIT\"\n" +
            "}, {" +
            "    \"description\" : \"Motor oil\",\n" +
            "    \"quantity\" : 1,\n" +
            "    \"price\" : 30.0,\n" +
            "    \"metric\" : \"LITER\"\n" +
            "  } ]\n" +
            "}, {" +
            "  \"vinForRepair\" : \"ABCDE123455432100\",\n" +
            "  \"dateOfRepair\" : \"32/x8/2021\",\n" +
            "  \"mileage\" : 250000,\n" +
            "  \"items\" : [ {\n" +
            "    \"description\" : \"Break fluid\",\n" +
            "    \"quantity\" : 1,\n" +
            "    \"price\" : 20.0,\n" +
            "    \"metric\" : \"LITER\"\n" +
            "  }, {" +
            "    \"description\" : \"Tyre\",\n" +
            "    \"quantity\" : 2,\n" +
            "    \"price\" : 80.0,\n" +
            "    \"metric\" : \"KABOOSH\"\n" +
            " } ]\n" +
            "}";


    static class OrderDto {

        public final String vinForRepair;
        public final LocalDate dateOfRepair;
        public final Long mileage;
        public final List<OrderItemDto> items;

        @JsonCreator
        public OrderDto(@JsonProperty("vinForRepair") String vinForRepair,
                        @JsonProperty("dateOfRepair") LocalDate dateOfRepair,
                        @JsonProperty("mileage") Long mileage,
                        @JsonProperty("items") List<OrderItemDto> items) {
            this.vinForRepair = vinForRepair;
            this.dateOfRepair = dateOfRepair;
            this.mileage = mileage;
            this.items = items;
        }

        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }
    }

    enum MetricEnum {
        UNIT, LITER
    }

    static class OrderItemDto {
        public final String description;
        public final Integer quantity;
        public final Double price;
        public final MetricEnum metric;

        @JsonCreator
        public OrderItemDto(@JsonProperty("description") String description,
                            @JsonProperty("quantity") Integer quantity,
                            @JsonProperty("price") Double price,
                            @JsonProperty("metric") MetricEnum metric) {
            this.description = description;
            this.quantity = quantity;
            this.price = price;
            this.metric = metric;
        }

        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }

    }

    static class OrderDtoDeserializer extends StdDeserializer<OrderDto> {

        public OrderDtoDeserializer(Class<?> vc) {
            super(vc);
        }

        public OrderDtoDeserializer() {
            this(null);
        }

        @Override
        public OrderDto deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            JsonNode jsonNode = jp.getCodec().readTree(jp);

            String vinForRepair = jsonNode.get("vinForRepair").textValue();
            LocalDate dateOfRepair = getDateOfRepair(jsonNode, ctxt);
            Long mileage = jsonNode.get("mileage").longValue();

            List<OrderItemDto> parsedItems = new ArrayList<>();

            JsonNode items = jsonNode.get("items");
            if (items.isArray()) {
                ArrayNode itemsArry = (ArrayNode)items;
                for (JsonNode item : itemsArry) {
                    String description = item.get("description").textValue();
                    Integer quantity = item.get("quantity").intValue();
                    Double price = item.get("price").doubleValue();
                    MetricEnum metric = getMetric(item, ctxt);
                    parsedItems.add(new OrderItemDto(description, quantity, price, metric));
                }
            }

            return new OrderDto(vinForRepair, dateOfRepair, mileage, List.copyOf(parsedItems));
        }

        //handle any weird values
        public LocalDate getDateOfRepair(JsonNode jsonNode, DeserializationContext ctxt) {
            String valueToConvert = jsonNode.get("dateOfRepair").textValue();
            try {
                return LocalDate.parse(valueToConvert, DateTimeFormatter.ofPattern("dd/MM/yyyy"));
            } catch (DateTimeParseException ex) {
                JsonLocation tokenLocation = ctxt.getParser().getTokenLocation();
                logger.warn("Bad input data on: [{},{}]:", tokenLocation.getLineNr(), tokenLocation.getColumnNr());
                logger.warn("{}. Will use default '{}' as date value instead.", ex.getMessage(), null);
                return null;
            }
        }

        //handle any weird values
        public MetricEnum getMetric(JsonNode item, DeserializationContext ctxt) {
            String valueToConvert = item.get("metric").textValue();
            try {
                return MetricEnum.valueOf(valueToConvert);
            } catch (IllegalArgumentException ex) {
                JsonLocation tokenLocation = ctxt.getParser().getTokenLocation();
                logger.warn("Bad input data on: [{},{}]:", tokenLocation.getLineNr(), tokenLocation.getColumnNr());
                logger.warn("Unknown '{}' value - {}. Will use default '{}' instead.", valueToConvert, ex.getMessage(), MetricEnum.UNIT);
                return MetricEnum.UNIT;
            }
        }
    }

    @Test
    public void deserialize() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();

        SimpleModule orderDtoModule = new SimpleModule();
        orderDtoModule.addDeserializer(OrderDto.class, new OrderDtoDeserializer());

        mapper.registerModules(new Jdk8Module(), orderDtoModule);

        CollectionType collectionType = mapper.getTypeFactory().constructCollectionType(List.class, OrderDto.class);

        //read from string
        List<OrderDto> orders = mapper.readValue("[" + payload + "]", collectionType);

        Assert.assertThat(orders, Matchers.notNullValue());
        Assert.assertThat(orders.size(), Matchers.is(2));

        logger.info("{}", orders);
    }
}

2 Comments

This works if all the data is correctly entered in the file, but if there is a single error in the file, for example - a wrong data field name, the whole List will not be created
I've edited my answer with an improved example.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.