从Spring的BindingResult到字段JSONPath/JSON Pointer,使用Jackson

huangapple go评论77阅读模式
英文:

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

问题

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).

英文:

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(&quot;Serialized: &quot; + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data));

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

    public static class Data {
        @JsonProperty(&quot;foobar&quot;)
        @Valid
        public List&lt;Foo&gt; foo = List.of(new Foo());
    }
    public static class Foo {
        @Min(100)
        public int barBaz = 42;
    }

}

Output:

Serialized: {
  &quot;foobar&quot; : [ {
    &quot;bar_baz&quot; : 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).

答案1

得分: 6

您可以轻松使用Hibernate Validator 6.1.5来实现这一点。

您需要提供自己的PropertyNodeNameProvider实现。

通过实现它,我们可以定义在验证期间如何解析属性的名称。在我们的情况下,我们希望从Jackson配置中读取该值。

创建一个验证器:

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

更多详情请参阅文档

英文:

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 -&gt; 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:

答案2

得分: 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."

"根据我理解你的问题,你正在寻找字段的路径。由于没有输入的 JSON,我为你提供了一个示例。"

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

"如果你对这个进行一些简单的字符串替换,我认为这是一个不错且简单的解决方案。我不确定是否可以使用纯粹的 Jackson/FasterXML 获得类似的结果。JsonPath 在底层使用了 Jackson。"

"如果你对这个进行一些简单的字符串替换,我认为这是一个不错且简单的解决方案。我不确定是否可以使用纯粹的 Jackson/FasterXML 获得类似的结果。JsonPath 在底层使用了 Jackson。"

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

"你可以在官方 git 仓库上了解更多关于 Jayway JsonPath 的信息。"

英文:

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 = &quot;{\&quot;top_field\&quot;: { \&quot;mid_field\&quot;: [ { \&quot;my_field\&quot;: true, }, { \&quot;my_field\&quot;: true, } ], \&quot;another_mid_field\&quot;: [ { \&quot;my_field\&quot;: false } ] }}&quot;;
		
		Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
		List&lt;String&gt; pathList = using(conf).parse(json).read(&quot;$..my_field&quot;);
		for(String path : pathList) {
			System.out.println(path);
		}
	}
}

Will output exactly

$[&#39;top_field&#39;][&#39;mid_field&#39;][0][&#39;my_field&#39;]
$[&#39;top_field&#39;][&#39;mid_field&#39;][1][&#39;my_field&#39;]
$[&#39;top_field&#39;][&#39;another_mid_field&#39;][0][&#39;my_field&#39;]

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

答案3

得分: 0

  1. 基于 @Lukasz 的回答,链接,这将在你提供 @JsonProperty() 时提供属性名称的蛇形命名或其他格式,例如:

    因此,将 @JsonProperty("bar_baz") 添加到 public int barBaz = 42; 并且 使用 JacksonPropertyNodeNameProvider 将会得到以下输出:

    路径:foobar[0].bar_baz

  1. 要将 foobar[0].bar_baz 转换为 JSON 路径,我认为字符串操作应该足够了。
   validator.validate(data).forEach(violation -> {
     System.out.println("路径:" + "$." + violation.getPropertyPath());
   });

最终输出将是

路径:$.foobar[0].bar_baz

英文:
  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(&quot;bar_baz&quot;) 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 -&gt; {
     System.out.println(&quot;Path: &quot; + &quot;$.&quot; + violation.getPropertyPath());
   });

And final output would be
> Path: $.foobar[0].bar_baz

huangapple
  • 本文由 发表于 2020年8月4日 21:57:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/63248487.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定