Appendix A: Value extraction for cascaded validation and type argument constraints (BVAL-508)

This appendix describes the current work-in-progress around the retrieval of values to be validated during cascaded validation (e.g. List<@Valid Order> orders) and evaluation of type argument constraints (e.g. Optional<@Email String> email) It is based on the original proposals for BVAL-508.

Motivation

Value extraction is needed when constraints are applied to the element(s) stored within a container type. There are two categories:

  • Cascaded validation of iterables, maps and arrays as triggered via @Valid: @Valid List<Order> orders; in this case all the values stored in the list must be extracted so each can be validated

  • Validation of type argument constraints as enabled by Java 8: Optional<@Email String> email or Property<@Min(1) Integer> value; in this case, e.g. the wrapped String and Integer values must be extracted so the @Email and @Min constraints can be applied

Both cases can also overlap: List<@RetailOrder @Valid Order> retailOrders.

Type argument constraints and pluggable extractors will also make cascaded validation more flexible. As of BV 1.1, there was a fixed set of data types supported by cascaded validation mandated by the spec (Iterable, arrays, Map (only the values would be validated). This excludes use cases such as custom collection types (e.g. Guava’s multi-map), validation of map keys or collection types of other JVM languages (such as Ceylon’s collection framework).

Allowing to put @Valid on type parameters allows to express the subject of cascaded validation more specifically: Map<@Valid AddressType, @Valid Address> addresses would describe that both, map keys and values, should be validated.

Requirements

  1. Constraints can be applied to type arguments (of bean properties, but also executable parameters and method return values): Property<@Min(1) Integer> value

  2. Individual type parameters can be marked for cascaded validation: Map<@Valid AddressType, Address> addresses

  3. How values are retrieved must be customizable by means of a pluggable value extractor mechanism, with a set of defined default extractors

  4. Sometimes constraints should apply to a value wrapped by a container type, but there is no type parameter to put the constraints to. JavaFX’s Property hierarchy is the most prominent example: @Email StringProperty emailProperty. In such case the value should be implicitly extracted from the container if unambiguously doable.

  5. Allow to configure type argument constraints and cascades via XML mappings

  6. Expose meta-data on type argument constraints and cascades in the constraint metadata API

Non-requirements

  1. Constraints on type uses in local variables, invocations etc: @NotNull String name = "Emmanuel";, new @NonEmpty @Readonly List<String>(myNonEmptyStringSet)

  2. constraints on type uses in type definitions:

class CustList extends List<@NotNull Customer> {
}

Solution

Value retrieval for cascaded validation and validation of type argument constraints is done via the ValueExtractor API:

Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/ValueExtractor.java[lines=7..8;15..-1]

An extractor is tied to one specific type parameter of the type from which it extracts values. The @ExtractedValue annotation is used to mark that type parameter.

As an example, this is how the implementation of the list extractor may look like:

class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> {

        @Override
        public void extractValues(List<?> originalValue, ValueReceiver receiver) {
                for ( int i = 0; i < originalValue.size(); i++ ) {
                        receiver.indexedValue( "<iterable element>", i, originalValue.get( i ) );
                }
        }
}

The right callback methods (indexedValue(), keyedValue() etc.) must be called in order to allow for proper construction of the property path as per the rules laid out in the BV 1.1 spec.

If a non-null value is passed for nodeName, a path node of type CONTAINER_ELEMENT will be appended to the property path. That’s desirable for collection types for instance. If null is passed, no node will be appended, resulting in the same path as if the constraint had been given on the element itself instead of a type parameter. That’s desirable for pure "wrapper types" such as Optional.

@ExtractedValue

The @ExtractedValue annotation is used to denote the element extracted by a given value extractor:

Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/ExtractedValue.java[lines=7..8;15..-1]

The @ExtractedValue annotation must be specified exactly once for a value extractor type. Thus the following extractor definition is illegal as it specifies @ExtractedValue several times:

public class DoubleExtractor implements ValueExtractor<Multimap<@ExtractedValue ?, @ExtractedValue ?>> { ... }

When defined for a generic type, only wildcard type arguments may be annotated with @ExtractedValue. Thus the following extractor definition is illegal:

public class StringListValueExtractor implements ValueExtractor<List<@ExtractedValue String>> { ... }

This implies that there may not be more than one extractor for a given generic type. I.e. there can be an extractor for List<?>, but not one for List<String> and one for List<Integer>.

In case an illegal value extractor definition is detected, a ValueExtractorDefinitionException is raised.

If a value extractor returns the entire extracted type itself (as it is the case for the built-in extractor for handling any non-collection object associations), the @ExtractedValue annotation is to be given on the extracted type itself:

class ObjectValueExtractor implements ValueExtractor<@ExtractedValue Object> {

        @Override
        public void extractValues(Object originalValue, ValueReceiver receiver) {
                receiver.value( null, originalValue );
        }
}

If a value extractor returns the elements of an array type, the @ExtractedValue annotation is to be given for the array type as follows:

class IntArrayValueExtractor implements ValueExtractor<int @ExtractedValue[]> {

        @Override
        public void extractValues(int[] originalValue, ValueReceiver receiver) {
                for ( int i = 0; i < originalValue.length; i++ ) {
                        receiver.indexedValue( "<iterable element>", i, originalValue[i] );
                }
        }
}
Motivation for callback-style API

Instead of returning the extracted values from the method call, implementations of ValueExtractor pass the extracted values to the given receiver callback. This helps to avoid object allocations and allows to handle the case of a single extracted value (Optional<T>) and multiple extracted values (List<T>) in a uniform fashion.

Extensions to Node

There is a new javax.validation.ElementKind, CONTAINER_ELEMENT. There is also a new Node sub-type, ContainerElementNode:

interface ContainerElementNode extends Node {

    /**
     * @return the type of the container the node is placed in, if contained in a container type such as
     * {@code Optional}, {@code List} or an array, {@code null} otherwise
     */
    Class<?> getContainerClass();

    /**
     * @return the index of the type parameter affected by the violated constraint in the container class
     */
    Integer getTypeArgumentIndex();
}

getContainerClass() and getTypeArgumentIndex() also need to be added to BeanNode and PropertyNode and will return the affected type parameter when violating a class-level or property constraint in the course of cascaded validation of a generic type, such as List or Map.

Default extractors

Compatible implementations provide extractors for the following types out of the box. They must invoke the right callback methods in order to ensure path nodes in the described form are appended:

  • Arrays of objects and all primitive data types

    • @Valid can be given for the array itself or for its component type. Both will cause the validation of all the array elements

    • If a constraint given for an array’s component type is validated, a node with the following properties will be added to the path:

      • name: "<iterable element>"

      • kind: CONTAINER_ELEMENT

      • isInIterable: false

      • index: the element’s index

      • key: null

      • containerClass: in the case of an array of Objects, Object[], in the case of an array of primitives, the type of the array (e.g. int[])

      • typeArgumentIndex: null

  • java.util.Iterable - excluding the case of java.util.List described below

    • When @Valid is given on the iterable element itself, the element and all its entries will be validated; this is to grant backwards compatibility with BV 1.1

    • When @Valid is given on the type parameter of an iterable element, all the entries will be validated.

    • When validating a type argument constraint for Iterable, a node with the following properties will be added to the path:

      • name: "<iterable element>"

      • kind: CONTAINER_ELEMENT

      • isInIterable: true

      • index: the element’s index if the iterable is of type List or a subtype thereof; null otherwise

      • key: null

      • containerClass: Iterable

      • typeArgumentIndex: 0

  • java.util.List

    • When @Valid is given on the iterable element itself, the element and all its entries will be validated; this is to grant backwards compatibility with BV 1.1

    • When @Valid is given on the type parameter of an iterable element, all the entries will be validated.

    • When validating a type argument constraint for List, a node with the following properties will be added to the path:

      • name: "<iterable element>"

      • kind: CONTAINER_ELEMENT

      • isInIterable: true

      • index: the element’s index

      • key: null

      • containerClass: List

      • typeArgumentIndex: 0

  • java.util.Map

    • When @Valid is given on the map element itself, the element and all its values will be validated; this is to grant backwards compatibility with BV 1.1

    • When @Valid is given on the key type parameter, the map keys will be validated

    • When @Valid is given on the value type parameter, the map values will be validated

    • When validating a constraint on the key type argument of Map, a node with the following properties will be added to the path:

      • name: "<map key>"

      • kind: CONTAINER_ELEMENT

      • isInIterable: true

      • index: null

      • key: key

      • containerClass: Map

      • typeArgumentIndex: 0

    • When validating a constraint on the value type argument of Map, a node with the following properties will be added to the path:

      • name: "<map value>"

      • kind: CONTAINER_ELEMENT

      • isInIterable: true

      • index: null

      • key: key

      • containerClass: Map

      • typeArgumentIndex: 1

  • java.util.Optional

    • No node will be appended to the path when validating type argument constraints on Optional

  • javafx.beans.observable.ObservableValue

  • java.lang.Object

    • When @Valid is given for an element, the element will be validated

Examples

TODO

Plugging in custom extractors

Additional value extractors can be plugged in when bootstrapping a Validator or ValidatorFactory, amending and/or overriding the set of built-in value extractors. The following ways to do so exist, in descending order of precedence:

  • Invoking the method ValidatorContext#addValueExtractor(ValueExtractor<?>) (to apply it for a single Validator)

  • Invoking the method Configuration#addValueExtractor(ValueExtractor<?>) (to apply it at the validation factory level)

  • Specifying the fully-qualified class name of one or several extractors in META-INF/validation.xml:

    <value-extractors>
        <value-extractor>com.example.MyExtractor</value-extractor>
    </value-extractors>
  • Placing a file named META-INF/services/javax.validation.valueextraction.ValueExtractor on the classpath, with the fully-qualified name(s) of one or more extractor implementations as its contents (i.e. employing the Java service loader mechanism)

A value extractor for a given type and extracted type parameter specified at a higher level overrides any other extractors for the same type and extracted type given at lower levels. If e.g. a value extractor defined as class MyListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> { …​ } is given via ValidatorContext#addValueExtractor(ValueExtractor<?>), it will take precedence over any other value extractor implementing List<@ExtractedValue ?> given via Configuration#addValueExtractor(ValueExtractor<?>), META-INF/validation.xml or the service loader mechanism as well as the built-in extractor for List elements.

Applying element-level constraints to wrapped elements

Sometimes there is no type parameter to put a constraint to, but still constraints should be applied to the wrapped value instead of the annotated element (a field, property getter, method return value or executable parameter). JavaFX’s property hierarchy falls into that category, as it defines specific Property sub-types which are not generic. As an example, consider a generic type Wrapper<T> and a non-generic sub-type StringWrapper which binds the type parameter <T> to String:

@Email StringWrapper email;

Two defined sub-types of javax.validation.Payload can be used to control the target of validation in such cases via the constraint’s payload() attribute:

Example 1. Payload types for unwrapping control
Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/Unwrapping.java[lines=7..8;11..-1]
@Email(payload = Unwrapping.Unwrap.class) StringWrapper email;

Value extractor definitions can be marked with the @UnwrapByDefault annotation so that constraints are automatically applied to the wrapped value if a constraint is found for an element handled by that extractor:

Example 2. @UnwrapByDefault annotation
Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/UnwrapByDefault.java[lines=7..8;16..-1]
@UnwrapByDefault
class WrapperExtractor implements ValueExtractor<Wrapper<@ExtractedValue ?>> {

        @Override
        public void extractValues(Wrapper originalValue, ValueReceiver receiver) {
                // ...
        }
}

If this extractor was identified as the single most-specific extractor for StringWrapper (see Retrieval of extractors), the @Email constraint above would automatically be applied to the wrapped string value.

In rare cases it may happen that a constraint should be applied to the wrapped value although an extractor exists. In this case the Unwrapping.Skip payload type can be specified for that constraint to prevent the unwrapping:

@NotNull(payload = Unwrapping.Skip.class) StringWrapper email;

For the sake of readability, when applying constraints to the elements of a generic container type, it is strongly recommended to put the constraints to the type argument instead of the element itself in conjunction with Unwrapping.Unwrap. I.e. you should prefer

List<@Email String> emails;

over

@Email(payload = Unwrapping.Unwrap.class)
List<String> emails;

Retrieval of extractors

When detecting a type argument constraint or cascade, the applicable extractor is determined as follows:

  1. Select all those value extractors which handle a type parameter that maps to the type argument annotated with the constraint or @Valid annotation; Example:

    • Given List<@Email String> emails and considering the default extractors listed above, only the extractors for List and Iterable are selected. The former handles the type parameter T of type List, which directly maps to the type argument annotated with @Email. The latter handles the type parameter E of type Iterable which (indirectly) maps to the annotated type argument (as List extends Iterable and binds its type parameter T to E from Iterable). Other extractors such as the ones for K and V of Map are dismissed, as they handle type parameters not mapping to the annotated type argument

    • Given

      interface ConfusingMap<K, V> extends Map<V, K> {}
      ConfusingMap<@Email String, String> map;

      And considering the default extractors listed above, only the extractor for the type parameter V of Map will be selected. This is because the @Email constraint is given for the type argument representing type parameter K of ConfusingMap which maps to type parameter V of Map.

  2. From the remaining candidate value extractors choose the one which is most specific to the container type declaring the annoted type argument. An extractor A is more specific than another extractor B if A extracts a subtype of the type extracted by B. Example:

    • When obtaining the extractor for type parameter constraint validation, the declared type of the validated element is considered. This is to be consistent with constraint validator resolution, which is based on the static type of elements, not the runtime type.

    • When obtaining the extractor for cascaded validation, the runtime type of the cascaded element is considered. This is to be consistent with the rules defined for property path construction which are based on the runtime type.

    • From the two extractors above, the one for List is chosen as List is a subtype of Iterable.

  3. If there are several extractors which are equally specific (e.g. several extractors for List), an UnexpectedTypeException is raised. TODO: apply rules similar to "ConstraintValidator resolution algorithm" and further clarify wording

When detecting a regular element-level constraint (i.e. non type argument constraint) the applicable value extractor, if any, is determined as follows:

  1. If the constraint carries the Unwrapping.Skip payload, don’t apply any value extractor

  2. Determine the set of uniquely mapping type parameters declared by the types in the element type’s type hierarchy; Examples:

    • element of type java.lang.String: () (empty set)

    • element of type java.lang.Iterable: (T)

    • element of type java.lang.Map: (K, V)

    • element of type java.util.Collection: (E) (as the type parameter E of Collection maps to T of Iterable, only the type parameter of the subtype is considered)

    • interface A<T> {}, interface B<U> {}, class C implements A<String>, B<Integer> {}; element of type C: (T, U) (two non mapping type parameters)

  3. If the constraint carries the Unwrapping.Unwrap payload:

    • If no type parameter or more than one type parameter was found in step 2, raise a ConstraintDeclarationException

    • Choose the most specific extractor matching the single type parameter found in step 2

    • If there are several extractors which are equally specific, a ConstraintDeclarationException is raised.

    • If there is exactly one remaining extractor, apply this extractor

    • Otherwise, a ConstraintDeclarationException is raised.

  4. If the constraint neither carries the Unwrapping.Unwrap nor the Unwrapping.Skip payload:

    • If no type parameter or more than one type parameter was found in step 2, don’t apply any value extractor

    • Choose the most specific extractor matching the single type parameter found in step 2

    • If there are multiple such extractors, a ConstraintDeclarationException is raised.

    • If there is exactly one remaining extractor and it is marked with UnwrapByDefault, apply this extractor

    • Otherwise, no extractor is applied

Implementation note

As extractor retrieval for type parameter constraints is done using the static type of constrained elements, the retrieval can be done once at initialization time and then be cached. This is not possible for retrieval of extractors for cascaded validation.

Examples

  • Applying a constraint to the value wrapped by a container type:

    Property<@Min(1) Integer> value;

    Note that @Valid is not required; the @Min constraint will be validated when the value property is subject to validation.

  • Applying constraints to each value in a collection type:

    List<@NotNull @Email String> emails;
  • Cascaded validation of the values in a collection type:

    List<@Valid Order> orders;

    This will validate the constraints on each Order element in the list.

  • The legacy style for cascaded validation is supported as well:

    @Valid List<Order> orders;

    This would also validate any constraints on a custom list type (e.g. MyList#getId()). TODO: we never clarified that in 1.1. Should it be made explicit?

  • Map validation with type argument constraints and cascading:

    @Valid
    Map<@RegExp(...) String, @RetailOrder Order> orders;

    This would validate the map’s keys against @RegExp, the map’s values against @RetailOrder and apply cascaded validation of the map values (as well as the map object itself).

  • When selecting extractors, type parameters must be thoroughly traced in the hierarchy. Consider this case where the order of the type parameters of Map is swapped in a sub-type:

    public class CrazyMap<K, V> implements Map<V, K> { ... }
    public class Example {
        private CrazyMap<@RegExp(...) String, @Min(0) Long> crazyMap = ...;
    }

    Assuming there is no dedicated extractor for CrazyMap but only the default ones for K and V of Map, extraction for @RegExp must happen via the default map value extractor and extraction for @Min via the default map key extractor.

    A type parameter in a sub-type may also map to several type parameters in a super-type:

    interface NumericMap<T extends Number> extends Map<T, T> {}
    private NumericMap<@Min(1) Integer> integerMap;

    The @Min constraint is to be applied to the map’s keys and values as the annotated type parameter maps to K and V of Map.

  • type argument constraints can be applied to the elements of Object arrays and arrays of any primitive type:

    String @Email[] emails;
    int @Min(1) [] positiveNumbers;
  • The extractor for cascaded validation is determined based on an element’s runtime type:

    Collection<@Valid Order> orders = new ArrayList<>();

    Here the most-specific extractor for the runtime type ArrayList must be applied, causing the property nodes of violations to have an index set (Node#getIndex()).

  • The container value passed to a value extractor is retrieved from the element that has the type argument carrying the constraint or @Valid annotation:

    private Map<String, @Valid @RetailOrder Order> ordersByName;
    
    public Map<@NotNull String, Order> getOrdersByName() {
        return ordersByName;
    }

When validating the @NotNull constraint, the map as returned by the getter will be passed to the map key extractor in order to obtain the map keys. When validating the @RetailOrder constraint and performing cascaded validation, the map as obtained directly from the field will be passed to the map value extractor in order to obtain the map values.

  • Custom extractor for a Tuple type:

    public interface Tuple<T1, T2> {
        T1 getFirst();
        T2 getSecond();
    }
    public class TupleFirstExtractor implements ValueExtractor<Tuple<@ExtractedValue ?, ?>> {
    
            @Override
            public void extractValues(Tuple<?, ?> originalValue, ValueReceiver receiver) {
                    receiver.value( "<first>", originalValue.getFirst() );
            }
    }
    public class TupleSecondExtractor implements ValueExtractor<Tuple<?, @ExtractedValue ?>> {
    
            @Override
            public void extractValues(Tuple<?, ?> originalValue, ValueReceiver receiver) {
                    receiver.value( "<second>", originalValue.getSecond() );
            }
    }
    private Tuple<@NotNull @Email String, @NotNull @Min(1) Integer> tuple;
Examples for extractor retrieval
  • The most specific extractor matching the constrained type argument is chosen:

    private List<@Email String> emails;

    Based on the algorithm described above and considering the mandated default extractors, only the extractor for List and Iterable are candidate extractors (all other extractors are defined for a type parameter not mapping to T of List). The extractor for List will be applied as it’s more specific than the extractor for Iterable (List is a subtype of Iterable).

  • Constraints targeting wrapped values can be given on the wrapping element. Let there be these definitions:

    class StringWrapper {
        String wrapped;
    };
    @UnwrapByDefault
    class StringWrapperExtractor implements ValueExtractor<@ExtractedValue StringWrapper> { ... }
    };
    private @Email StringWrapper email;

    The @Email constraint will be applied to the wrapped string and can be validated as the extractor defines that element-level constraints should be applied to the wrapped value.

    If the extractor were not decorated with @UnwrapByDefault an exception would be raised as there is no validator for @Email on StringWrapper.

    Unwrapping could be mandated explicitly in this case:

    @Email(payload = Unwrapping.Unwrap.class)
    private StringWrapper email;
Invalid examples
  • No most specific extractor can be found unambiguously:

    public interface CachedValue<V> {
        V getCachedValue();
    }
    public interface RealValue<V> {
        V getRealValue();
    }
    public class CachableValue<V> implements CachedValue<V>, RealValue<V> { ... }
    public class CachedValueExtractor implements ValueExtractor<CachedValue<@ExtractedValue ?>> { ... }
    public class RealValueExtractor implements ValueExtractor<RealValue<@ExtractedValue ?>> { ... }
    private CachableValue<@Min(1) Integer> foo;

    Validation of foo will fail, as none of the two matching extractors is more specific than the other one. An extractor for CachableValue must be added, resolving the ambiguity.

  • Element-level constraints cannot be applied if there is no type parameter at all or multiple non-mapping type parameters in the annotated element’s type hierarchy. Thus an exception will be raised in the following cases:

    // no type parameter
    @Email(payload = Unwrapping.Unwrap.class)
    private String email;
    // multiple type parameters
    @Email(payload = Unwrapping.Unwrap.class)
    private Map<String, String> emails;

XML based configuration

TODO

Metadata retrieval

TODO

Misc.

  • Regarding group sequences and default group sequences, the same rules apply for type argument constraints as they apply for regular element-level constraints.

  • For the conversion of validation groups the same rules apply no matter whether @Valid is given for a regular element or for a type argument. I.e. the following group conversion declaration is valid:

private List<@Valid @ConvertGroup(from=Default.class, to=Other.class) Order> orders;

Open questions

  1. Should nested containers be supported: List<Map<String, @NotNull String>> addressesByType? Or Optional<List<@Email>> optionalEmails; The latter seems very reasonable.

  2. ConstraintsApplyTo only allows one behavior per annotated element. Should it be per constraint? E.g. for @NotNull @Email StringProperty email it may be desirable to apply @NotNull to the wrapper but @Email to the wrapped value. That’s not possible currently.

    For the first draft we settled for using dedicated Payload types to control the unwrapping behavior. While not ideal, this provides the required level of control. Discussed alternatives include a new constraint member boolean validateWrappedValue(), but this requires to update every existing constraint and adds a lot of "weight" for a rather small corner case. Any approach using a separate annotation fails as there is no way to clearly identify the targeted constraint annotation, as annotations cannot obtained from an element in a guaranteed order.

  3. Should ConstraintsApplyTo also be used for tagging extractors triggering "auto-extraction". Maybe a separate annotation would be less confusing, e.g. @AutoExtract or so?

    This is done by @UnwrapByDefault now which solely exists for this purpose.

  4. Should a path node be added for type argument constraints of Optional and similar types?

    This proposal suggests to not do it, but Emmanuel is not convinced of this.

  5. Should value extractors be discoverable via the service loader mechanism (i.e. by means of META-INF/services/javax.validation.valueextraction.ValueExtractor files)

    Pro: It’d allow 3rd party libs such as Google Guava to provide custom extractors for their container types and have them automatically be applied without any effort for the user.

    Cons: Need a way to disable or override some extractors with others. Which might make it a nogo.

    Added service loader mechanism as source of value extractors. Specified order of precedence.

  6. What to return from PropertyDescriptor#getElementClass() if there is a field of type Foo but a getter of type Optional<Foo>. So far, BV assumed the types of field and getter to be the same and exposed a single property descriptor (which btw. also may fall apart as of BV 1.1 when the field is of a sub-type of the getter’s type). What to return here?

  7. Should the presence of type argument constraints alone trigger cascaded validation?

    E.g. consider the case of Tuple above:

    Tuple<@Min(1) Integer, @Email String> tuple;

    Here it may be nice to validate e.g. @NotNull constraints given within the Tuple class itself when validating the type argument constraints. With the current proposal their validation requires a separate @Valid on the element. Personally I think that’s better (more consistent).

    We agreed to require @Valid for the sake of consistency.

  8. For an element with a type argument, should it be allowed to specify constraints on the element (and use the Unwrapping.Unwrap payload) or should it be disallowed?

    @Email(payload = Unwrapping.Unwrap.class) Optional<String> email;
  9. Should we allow extractors to be defined for specific parameterized types, e.g.:

    public class ListOfIntegerExtractor implements ValueExtractor<List<@ExtractedValue Integer>> { ... }
    
    public class ListOfStringExtractor implements ValueExtractor<List<@ExtractedValue String>> { ... }

    Currently, only one extractor (for type List<?> is allowed).

    I can’t see a compelling use case for this (when would extractor behavior differ between different parameterizations of the same generic type) and am leaning towards only supporting the wildcard parameterization (implements ValueExtractor<List<@ExtractedValue ?>>).

    We agreed on only allowing one extractor per type, to be given using the wildcard parameterization, as we cannot see a use case for having multiple extractors right now. But if needed, this limitation can be lifted in a future revision.

  10. Can we find another name than "type argument constraints"? While that suits for the most cases, it doesn’t when applying constraints to the component type of an array: String @NotBlank [] names.

    I think "type use" is the correct one in Java terminology. But would anyone get what a "type use constraint" is?.

  11. Vet the API by exploring advanced use cases, e.g. Guava’s Table, Graph, ValueGraph and Network types.

    Example for Table:

    class TableValueExtractor implements ValueExtractor<Table<?, ?, @ExtractedValue ?>> {
    
            @Override
            public void extractValues(Table<?, ?, ?> originalValue, ValueExtractor.ValueReceiver receiver) {
    
                    for ( Cell<?, ?, ?> cell : originalValue.cellSet() ) {
                            receiver.keyedValue(
                                            "<table cell>",
                                            new CellKey( cell.getRowKey(), cell.getColumnKey() ),
                                            cell.getValue()
                            );
                    }
            }
    }
    public static class CellKey {
    
            private final Object rowKey;
            private final Object columnKey;
    
            public CellKey(Object rowKey, Object columnKey) {
                    this.rowKey = rowKey;
                    this.columnKey = columnKey;
            }
    
            // equals(), hashCode() ...
    }

    When having an invalid table cell in the following and validating it:

    public class Customer {
    
        Table<Year, String, @Min(1) Integer> revenuePerYearAndCategory = HashBasedTable.create();
    }

    Then this will be the result:

    ConstraintViolation<Customer> violation = ...;
    
    Iterator<Node> path = violation.getPropertyPath().iterator();
    
    Node node = path.next();
    assertThat( node.getName() ).isEqualTo( "revenuePerYearAndCategory" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY );
    assertThat( node.getKey() ).isNull();
    assertThat( node.getIndex() ).isNull();
    
    node = path.next();
    assertThat( node.getName() ).isEqualTo( "<table cell>" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.CONTAINER_ELEMENT );
    assertThat( node.getKey() ).isEqualTo( new CellKey(Year.of( 2015 ), "cds") );
    assertThat( node.getIndex() ).isNull();
    
    assertThat( path.hasNext() ).isFalse();
  12. In the original proposal it was foreseen that @ExtractedValue could refer to type-parameters from super-types. Is that still needed?

  13. During cascaded validation of an element with several type arguments, it’s currently not possible to tell from the resulting constraint violation and its node path which type argument was cascaded. Example:

    Map<@Valid OrderType, @Valid Order> ordersByType;

    If there was constraint violation on an OrderType property and one on an Order property, one couldn’t tell from the resulting paths and their nodes which is which.

    One way out could be to add TypeVariable<?> Node#getTypeParameter() .

    This would return the type parameter handled by the extractor used for obtaining the cascaded value. Node#getTypeParameter() would also return the type parameter for type argument constraints. Note it must be getTypeParameter() (not getTypeArgument()) because one annotated type argument at the constrained/cascaded element could represent multiple type parameters (see example "A type parameter in a sub-type may also map to several type parameters in a super-type" above). Assuming a constraint violation on property description of class OrderType we’d get:

    ConstraintViolation<Customer> violation = ...;
    
    Iterator<Node> path = violation.getPropertyPath().iterator();
    
    Node node = path.next();
    assertThat( node.getName() ).isEqualTo( "ordersByType" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY );
    assertThat( node.getKey() ).isNull();
    assertThat( node.getIndex() ).isNull();
    assertThat( node.getTypeParameter() ).isNull();
    
    node = path.next();
    assertThat( node.getName() ).isEqualTo( "description" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY );
    assertThat( node.getKey() ).isEqualTo( new OrderType( "RETAIL" ) );
    assertThat( node.getIndex() ).isNull();
    assertThat( node.getTypeParameter().getName() ).isEqualTo( "K" );
    
    assertThat( path.hasNext() ).isFalse();

    getTypeParameter() would also return the type parameter in case of type argument constraints:

    Map<OrderType, @Min(1) Integer> orderQuantitiesByType;
    ConstraintViolation<Customer> violation = ...;
    
    Iterator<Node> path = violation.getPropertyPath().iterator();
    
    Node node = path.next();
    assertThat( node.getName() ).isEqualTo( "orderQuantitiesByType" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY );
    assertThat( node.getKey() ).isNull();
    assertThat( node.getIndex() ).isNull();
    assertThat( node.getTypeParameter() ).isNull();
    
    node = path.next();
    assertThat( node.getName() ).isEqualTo( "<map value>" );
    assertThat( node.getKind() ).isEqualTo( ElementKind.CONTAINER_ELEMENT );
    assertThat( node.getKey() ).isEqualTo( new OrderType( "RETAIL" ) );
    assertThat( node.getIndex() ).isNull();
    assertThat( node.getTypeParameter().getName() ).isEqualTo( "V" );
    
    assertThat( path.hasNext() ).isFalse();
  14. Revisit the names of nodes of kind CONTAINER_ELEMENT; instead of <iterable element>, <map value> etc. should it be the names of the type parameters in question, i.e. <E>, <V> etc.?