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