Skip to content

Commit 0974534

Browse files
Damir Alibegovicgsmet
Damir Alibegovic
authored andcommitted
HV-823 Provide contract for customization of property names in constraint violation
Added PropertyNodeNameProvider SPI with Property and JavaBeanProperty as supporting interfaces. This SPI lives in JavaBeanHelper and is used to get the name when creating JavaBeanField and JavaBeanGetter, so when a property path is constructed, this resolved name is used. If not set, the default implementation will be used that returns the actual name from the class. This new SPI can be configured through HibernateValidatorConfiguration. Testing: - Added tests for configuration - Added a sample implementation by using reflection and custom annotation - Added tests for reflection implementation - Added a sample implementation by using Jackson lib - Added tests for Jackson implementation Added documentation with examples.
1 parent c9f7a0b commit 0974534

File tree

43 files changed

+1070
-29
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1070
-29
lines changed

copyright.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Carlo de Wolf
1111
Chris Beckey
1212
Christian Ivan
1313
Dag Hovland
14+
Damir Alibegovic
1415
Davide D'Alto
1516
Davide Marchignoli
1617
Denis Tiago

documentation/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@
103103
<artifactId>javax.annotation-api</artifactId>
104104
<scope>test</scope>
105105
</dependency>
106+
<dependency>
107+
<groupId>com.fasterxml.jackson.core</groupId>
108+
<artifactId>jackson-databind</artifactId>
109+
<scope>test</scope>
110+
</dependency>
111+
<dependency>
112+
<groupId>com.fasterxml.jackson.core</groupId>
113+
<artifactId>jackson-annotations</artifactId>
114+
<scope>test</scope>
115+
</dependency>
106116
</dependencies>
107117

108118
<build>

documentation/src/main/asciidoc/ch12.asciidoc

+94-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Note that when a package is part of the public API this is not necessarily true
5151
`org.hibernate.validator.spi.constraintdefinition`::
5252
An SPI for registering additional constraint validators programmatically, see <<section-constraint-definition-contribution>>.
5353

54+
`org.hibernate.validator.spi.nodenameprovider`::
55+
An SPI that can be used to alter how the names of properties will be resolved when the property path is constructed. See <<section-property-node-name-provider>>.
56+
5457
[NOTE]
5558
====
5659
The public packages of Hibernate Validator fall into two categories: while the actual API parts are
@@ -759,4 +762,94 @@ It is important to mention that in cases where programmatic constraints are adde
759762
`HibernateValidatorConfiguration#addMapping(ConstraintMapping)`, adding mappings should
760763
always be done after the required getter property selection strategy is configured.
761764
Otherwise, the default strategy will be used for the mappings added before defining the strategy.
762-
====
765+
====
766+
767+
[[section-property-node-name-provider]]
768+
==== Changing the way property names are resolved when the property path is constructed
769+
770+
Imagine that we have a simple data class that has `@NotNull` constraints on some fields:
771+
[[example-person-class]]
772+
.Person data class
773+
====
774+
[source, JAVA, indent=0]
775+
----
776+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/Person.java[tags=include]
777+
----
778+
====
779+
780+
This class can be serialized to JSON by using the https://github.com/FasterXML/jackson[Jackson] library:
781+
[[example-person-object-to-json]]
782+
.Serializing Person object to JSON
783+
====
784+
[source, JAVA, indent=0]
785+
----
786+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/PersonSerializationTest.java[tags=include]
787+
----
788+
====
789+
790+
As we can see, the object is serialized to:
791+
[[example-person-json]]
792+
.Person as json
793+
====
794+
[source, indent=0]
795+
----
796+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/clarkKent.json[]
797+
----
798+
====
799+
800+
Notice how the names of the properties differ. In the Java object, we have `firstName` and `lastName`, whereas in the JSON output, we have
801+
`first_name` and `last_name`. We defined this behaviour through `@JsonProperty` annotations.
802+
803+
Now imagine, that we use this class in a REST environment, where a user can send <<example-person-json, person as json>> in the request body.
804+
It would be nice, when telling the user on which field the validation failed, to tell them the name they use in their JSON request, `first_name`,
805+
and not the name we use internally in our Java code, `firstName`.
806+
807+
The `org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider` contract allows us to do this. By implementing it,
808+
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.
809+
810+
So, one example of how to do this is to leverage the Jackson API:
811+
[[example-jackson-property-node-name-provider]]
812+
.JacksonPropertyNodeNameProvider implementation
813+
====
814+
[source, JAVA, indent=0]
815+
----
816+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/JacksonPropertyNodeNameProvider.java[tags=include]
817+
----
818+
====
819+
820+
And when doing the validation:
821+
[[example-jackson-property-node-name-provider-field]]
822+
.JacksonPropertyNodeNameProvider usage
823+
====
824+
[source, JAVA, indent=0]
825+
----
826+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/JacksonPropertyNodeNameProviderTest.java[tags=field]
827+
----
828+
====
829+
830+
We can see that the property path now returns `first_name`.
831+
832+
Note that this also works when the annotations are on the getter:
833+
[[example-jackson-property-node-name-provider-getter]]
834+
.Annotation on a getter
835+
====
836+
[source, JAVA, indent=0]
837+
----
838+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/nodenameprovider/JacksonPropertyNodeNameProviderTest.java[tags=getter]
839+
----
840+
====
841+
842+
This is just one use case of why we would like to change how the property names are resolved.
843+
844+
`org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider` can be implemented to provide a property name in
845+
whatever way you see fit (reading from annotations, for instance).
846+
847+
There are two more interfaces that are worth mentioning:
848+
849+
- `org.hibernate.validator.spi.nodenameprovider.Property` is a base interface that holds metadata about a property. It
850+
has a single `String getName()` method that can be used to get the "original" name of a property. This interface
851+
should be used as a default way of resolving the name (see how it is used in <<example-jackson-property-node-name-provider>>).
852+
853+
- `org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty` is an interface that holds metada about a bean property. It
854+
extends `org.hibernate.validator.spi.nodenameprovider.Property` and provide some more data like `Class<?> getDeclaringClass()`
855+
which is the class that is the owner of the property. This additional metadata can be useful when revolving the name.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.hibernate.validator.referenceguide.chapter12.nodenameprovider;
2+
3+
//tag::include[]
4+
import org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty;
5+
import org.hibernate.validator.spi.nodenameprovider.Property;
6+
import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider;
7+
8+
import com.fasterxml.jackson.databind.BeanDescription;
9+
import com.fasterxml.jackson.databind.JavaType;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
12+
13+
public class JacksonPropertyNodeNameProvider implements PropertyNodeNameProvider {
14+
private final ObjectMapper objectMapper = new ObjectMapper();
15+
16+
@Override
17+
public String getName(Property property) {
18+
if ( property instanceof JavaBeanProperty ) {
19+
return getJavaBeanPropertyName( (JavaBeanProperty) property );
20+
}
21+
22+
return getDefaultName( property );
23+
}
24+
25+
private String getJavaBeanPropertyName(JavaBeanProperty property) {
26+
JavaType type = objectMapper.constructType( property.getDeclaringClass() );
27+
BeanDescription desc = objectMapper.getSerializationConfig().introspect( type );
28+
29+
return desc.findProperties()
30+
.stream()
31+
.filter( prop -> prop.getInternalName().equals( property.getName() ) )
32+
.map( BeanPropertyDefinition::getName )
33+
.findFirst()
34+
.orElse( property.getName() );
35+
}
36+
37+
private String getDefaultName(Property property) {
38+
return property.getName();
39+
}
40+
}
41+
//end::include[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.hibernate.validator.referenceguide.chapter12.nodenameprovider;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.util.Set;
6+
import javax.validation.ConstraintViolation;
7+
import javax.validation.Validation;
8+
import javax.validation.Validator;
9+
import javax.validation.ValidatorFactory;
10+
import javax.validation.constraints.NotNull;
11+
12+
import org.hibernate.validator.HibernateValidator;
13+
14+
import org.junit.Test;
15+
16+
import com.fasterxml.jackson.annotation.JsonProperty;
17+
18+
//tag::field[]
19+
public class JacksonPropertyNodeNameProviderTest {
20+
@Test
21+
public void nameIsReadFromJacksonAnnotationOnField() {
22+
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
23+
.configure()
24+
.propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
25+
.buildValidatorFactory();
26+
27+
Validator validator = validatorFactory.getValidator();
28+
29+
Person clarkKent = new Person( null, "Kent" );
30+
31+
Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
32+
ConstraintViolation<Person> violation = violations.iterator().next();
33+
34+
assertEquals( violation.getPropertyPath().toString(), "first_name" );
35+
}
36+
//end::field[]
37+
38+
//tag::getter[]
39+
@Test
40+
public void nameIsReadFromJacksonAnnotationOnGetter() {
41+
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
42+
.configure()
43+
.propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
44+
.buildValidatorFactory();
45+
46+
Validator validator = validatorFactory.getValidator();
47+
48+
Person clarkKent = new Person( null, "Kent" );
49+
50+
Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
51+
ConstraintViolation<Person> violation = violations.iterator().next();
52+
53+
assertEquals( violation.getPropertyPath().toString(), "first_name" );
54+
}
55+
56+
public class Person {
57+
private final String firstName;
58+
59+
@JsonProperty("last_name")
60+
private final String lastName;
61+
62+
public Person(String firstName, String lastName) {
63+
this.firstName = firstName;
64+
this.lastName = lastName;
65+
}
66+
67+
@NotNull
68+
@JsonProperty("first_name")
69+
public String getFirstName() {
70+
return firstName;
71+
}
72+
}
73+
//end::getter[]
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.hibernate.validator.referenceguide.chapter12.nodenameprovider;
2+
3+
import javax.validation.constraints.NotNull;
4+
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
//tag::include[]
8+
public class Person {
9+
@NotNull
10+
@JsonProperty("first_name")
11+
private final String firstName;
12+
13+
@JsonProperty("last_name")
14+
private final String lastName;
15+
16+
public Person(String firstName, String lastName) {
17+
this.firstName = firstName;
18+
this.lastName = lastName;
19+
}
20+
}
21+
//end::include[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.hibernate.validator.referenceguide.chapter12.nodenameprovider;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import org.junit.Test;
6+
7+
import com.fasterxml.jackson.core.JsonProcessingException;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
10+
//tag::include[]
11+
public class PersonSerializationTest {
12+
private final ObjectMapper objectMapper = new ObjectMapper();
13+
14+
@Test
15+
public void personIsSerialized() throws JsonProcessingException {
16+
Person person = new Person( "Clark", "Kent" );
17+
18+
String serializedPerson = objectMapper.writeValueAsString( person );
19+
20+
assertEquals( "{\"first_name\":\"Clark\",\"last_name\":\"Kent\"}", serializedPerson );
21+
}
22+
}
23+
//tag::include[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"first_name": "Clark",
3+
"last_name": "Kent"
4+
}

engine/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@
147147
<artifactId>moneta</artifactId>
148148
<scope>test</scope>
149149
</dependency>
150+
<dependency>
151+
<groupId>com.fasterxml.jackson.core</groupId>
152+
<artifactId>jackson-databind</artifactId>
153+
<scope>test</scope>
154+
</dependency>
155+
<dependency>
156+
<groupId>com.fasterxml.jackson.core</groupId>
157+
<artifactId>jackson-annotations</artifactId>
158+
<scope>test</scope>
159+
</dependency>
150160
</dependencies>
151161

152162
<build>

engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java

+24
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.Set;
1111

1212
import javax.validation.Configuration;
13+
import javax.validation.ConstraintViolation;
1314
import javax.validation.TraversableResolver;
1415
import javax.validation.constraints.Future;
1516
import javax.validation.constraints.FutureOrPresent;
@@ -20,6 +21,7 @@
2021
import org.hibernate.validator.cfg.ConstraintMapping;
2122
import org.hibernate.validator.constraints.ParameterScriptAssert;
2223
import org.hibernate.validator.constraints.ScriptAssert;
24+
import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider;
2325
import org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy;
2426
import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator;
2527
import org.hibernate.validator.spi.scripting.ScriptEvaluator;
@@ -119,6 +121,15 @@ public interface BaseHibernateValidatorConfiguration<S extends BaseHibernateVali
119121
@Incubating
120122
String GETTER_PROPERTY_SELECTION_STRATEGY_CLASSNAME = "hibernate.validator.getter_property_selection_strategy";
121123

124+
/**
125+
* Property for configuring the property node name provider, allowing to select an implementation of {@link PropertyNodeNameProvider}
126+
* which will be used for property name resolution when creating a property path.
127+
*
128+
* @since 6.1.0
129+
*/
130+
@Incubating
131+
String PROPERTY_NODE_NAME_PROVIDER_CLASSNAME = "hibernate.validator.property_node_name_provider";
132+
122133
/**
123134
* <p>
124135
* Returns the {@link ResourceBundleLocator} used by the
@@ -336,4 +347,17 @@ public interface BaseHibernateValidatorConfiguration<S extends BaseHibernateVali
336347
*/
337348
@Incubating
338349
S getterPropertySelectionStrategy(GetterPropertySelectionStrategy getterPropertySelectionStrategy);
350+
351+
/**
352+
* Allows to set a property node name provider, defining how the name of a property node will be resolved
353+
* when constructing a property path as the one returned by {@link ConstraintViolation#getPropertyPath()}.
354+
*
355+
* @param propertyNodeNameProvider the {@link PropertyNodeNameProvider} to be used
356+
*
357+
* @return {@code this} following the chaining method pattern
358+
*
359+
* @since 6.1.0
360+
*/
361+
@Incubating
362+
S propertyNodeNameProvider(PropertyNodeNameProvider propertyNodeNameProvider);
339363
}

0 commit comments

Comments
 (0)