From Spring BindingResult to field JSONPath/JSON Pointer, with Jackson
Asked Answered
T

3

7

I have a Spring Boot application using javax.validation annotations and I'm trying to return friendly JSON error messages pointing to the offending field, yet converting from the available "Java-object" path to either JSONPath or JSON Pointer is something I'm not finding a way to do.

SSCO sample:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;

import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.Min;
import java.util.List;

public class Test {

    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

        Data data = new Data();
        System.out.println("Serialized: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data));

        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        validator.validate(data).forEach(violation -> {
            System.out.println("Path: " + violation.getPropertyPath());
        });
    }

    public static class Data {
        @JsonProperty("foobar")
        @Valid
        public List<Foo> foo = List.of(new Foo());
    }
    public static class Foo {
        @Min(100)
        public int barBaz = 42;
    }

}

Output:

Serialized: {
  "foobar" : [ {
    "bar_baz" : 42
  } ]
}
Path: foo[0].barBaz

As you can see, I need to convert foo[0].barBaz into either $.foobar[0].bar_baz or /foobar/0/bar_baz. The parsed object (the data variable above) is also provided by the BindingResult object that holds the validation information.

I thought about doing some String manipulation, but that's messy, hacky, and can break easily with @JsonProperty which I would need to handle separately, maybe other corner cases that I didn't think about. Plus, we use SNAKE_CASE as a standard, changing to simplify the task is not a solution.

I suppose Jackson's ObjectMapper could be used somehow to make this conversion, or some other piece of Jackson API, but I couldn't find anything about that. Any other library that can do this is also fine (ideally it should understand Jackson annotations like @JsonProperty).

Turbid answered 4/8, 2020 at 13:57 Comment(0)
D
6

You can do it easily with Hibernate Validator 6.1.5.

You need to provide your own implementation of PropertyNodeNameProvider.

By implementing it, we can define how the name of a property will be resolved during validation. In our case, we want to read the value from the Jackson configuration.

Creating a validator:

 ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .propertyNodeNameProvider(new JacksonPropertyNodeNameProvider())
        .buildValidatorFactory();

JacksonPropertyNodeNameProvider:

public class JacksonPropertyNodeNameProvider implements PropertyNodeNameProvider {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public String getName(Property property) {
    if ( property instanceof JavaBeanProperty ) {
        return getJavaBeanPropertyName( (JavaBeanProperty) property );
    }

    return getDefaultName( property );
  }

  private String getJavaBeanPropertyName(JavaBeanProperty property) {
    JavaType type = objectMapper.constructType( property.getDeclaringClass() );
    BeanDescription desc = objectMapper.getSerializationConfig().introspect( type );

    return desc.findProperties()
            .stream()
            .filter( prop -> prop.getInternalName().equals( property.getName() ) )
            .map( BeanPropertyDefinition::getName )
            .findFirst()
            .orElse( property.getName() );
  }

  private String getDefaultName(Property property) {
    return property.getName();
  }
}

More details You can find in documentation:

Deprecatory answered 12/8, 2020 at 12:47 Comment(1)
Thanks, your code works perfectly. For such a task I expected to have this out-of-the-box, but it's good that can be constructed easily.Turbid
M
0

As far as I understood your question You are seeking path to your field. As there is no I/P JSON I have taken one example for you.

package jsonpath;

import java.util.List;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Option;
import static com.jayway.jsonpath.JsonPath.*;

public class GetPaths {
    
    public static void main(String [] args) {
        String json = "{\"top_field\": { \"mid_field\": [ { \"my_field\": true, }, { \"my_field\": true, } ], \"another_mid_field\": [ { \"my_field\": false } ] }}";
        
        Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
        List<String> pathList = using(conf).parse(json).read("$..my_field");
        for(String path : pathList) {
            System.out.println(path);
        }
    }
}

Will output exactly

$['top_field']['mid_field'][0]['my_field']
$['top_field']['mid_field'][1]['my_field']
$['top_field']['another_mid_field'][0]['my_field']

If you do some simple string replace on that one I think it´s a nice and easy solution. I`m not sure if you can get anything similar with plain Jackson/FasterXML. JsonPath uses Jackson under the hood.

You can get to more about Jayway JsonPath on official git repo

Marshallmarshallese answered 12/8, 2020 at 11:45 Comment(2)
I guess OP doesn't have a json. Instead, he has Data java object. When the validator fails, he has a java property path but he wants to translate it to json path taking into account of annotations in the Data java objectPennate
I Doubt. He is throwing JsonProcessingException means there is JSON. Rest question can be improved. There is lot of theory.... Thanks for input @Kavithakaran KanapathippillaiMarshallmarshallese
R
0
  1. Building on @Lukasz's answer, which will give you property name in snake case or whatever format you want but only when you provide @JsonProperty().

    So, adding @JsonProperty("bar_baz") to public int barBaz = 42; AND using JacksonPropertyNodeNameProvider will give you following output.

    Path: foobar[0].bar_baz

  1. To convert foobar[0].bar_baz to jsonpath, I think String manipulation should suffice.
   validator.validate(data).forEach(violation -> {
     System.out.println("Path: " + "$." + violation.getPropertyPath());
   });

And final output would be

Path: $.foobar[0].bar_baz

Romanticist answered 14/8, 2020 at 12:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.