ViolationMessage.of() with arguments
Dear Maki,
Some times need to create messages and I use ViolationMessage.of() for that.
However ViolationMessage doesn't accept arguments. This limits me to only have plain messages.
For example consider a message as follows: "Please provide a number between {0} and {1}" and I need to send the arguments to be resolved. Some times I use the messageKey and I would need to have the arguments available to be resolved in a GlobalExceptionHandler with a MessageSource to be translated.
Currently I am addressing that with CustomConstraints but It would be useful to have another constructor to resolve te argumentos in ViolationMessage.of()
I understand that in CustomConstraint the first attribute is the name so {0} is reserved.
My suggestion: ViolationMessage.of(String messageKey, String defaultMessageFormat, Object[] args)
What are you thoughts on this?
I don't understand the case where a ViolationMessage with arguments is needed. I think the intended use is to use a CustomConstatraint. Can you provide the full code for your use case?
Dear Maki,
Sorry for the delay in replying.
It would be helpful to have a way to provide the ViolationMessage the arguments so that I I can then generated a template message that could use them. Also my main go is to internationalize the message at some point.
Consider the following code:
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.constraint.charsequence.CodePoints;
import am.ik.yavi.core.Constraint;
import am.ik.yavi.core.ConstraintContext;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.CustomConstraint;
import am.ik.yavi.core.Validator;
import am.ik.yavi.core.ViolationMessage;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.Serializable;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
record Description( String value) implements Serializable {
}
@Slf4j
class DescriptionValidatorTest {
@Test
@DisplayName("Should fail validation for a description longer than 10 characters")
void validateTooLongDescription() {
Validator<Description> validator = ValidatorBuilder.of(Description.class)
._string(Description::value, "description", c -> c.codePoints(CodePoints.Range.of(5, 10))
.asWhiteList()
.message(ViolationMessage.of("DESCRIPTION_LENGTH_INVALID", "Description must be between {1} and {2} characters")))
.build();
String longDescription = "a".repeat(11);
Description description = new Description(longDescription);
List<ConstraintViolation> violations = validator.validate(description);
assertThat(violations).hasSize(1);
Object[] args = violations.getFirst().args();
log.info("args content: {}", args);
log.info("args length: {}", args.length);
assertThat(violations.getFirst().messageKey()).isEqualTo("DESCRIPTION_LENGTH_INVALID");
assertThat(violations.getFirst().message()).isEqualTo("Description must be between {1} and {2} characters");
Assertions.assertThat(args).hasSize(2).contains(5, 3);
}
@Test
@DisplayName("Should fail validation for a description longer than 10 characters")
void validateTooLongWithCustomConstraintDescription() {
Validator<Description> validator = ValidatorBuilder.of(Description.class)
.constraint(Description::value, "description", Constraint::notNull)
.constraintOnCondition(this::isDescriptionNotEmpty, b ->
b.constraintOnTarget(new DescriptionLengthConstraint(5, 10), "description"))
.build();
String longDescription = "a".repeat(11);
Description description = new Description(longDescription);
List<ConstraintViolation> violations = validator.validate(description);
assertThat(violations).hasSize(1);
Object[] args = violations.getFirst().args();
log.info("args length: {}", args.length);
for (int i = 0; i < args.length; i++) {
log.info("arg[{}]: {}", i, args[i]);
}
assertThat(violations.getFirst().messageKey()).isEqualTo("DESCRIPTION_LENGTH_INVALID");
assertThat(violations.getFirst().message()).isEqualTo("Description must be between 5 and 10 characters");
}
private boolean isDescriptionNotEmpty(Description description, ConstraintContext constraintGroup) {
return !description.value().isEmpty();
}
public static class DescriptionLengthConstraint implements CustomConstraint<Description> {
private final int min;
private final int max;
/**
* Constructs a new DescriptionLengthConstraint.
*
* @param min minimum allowed length
* @param max maximum allowed length
*/
public DescriptionLengthConstraint(int min, int max) {
this.min = min;
this.max = max;
}
@Override
public boolean test(Description description) {
if (description == null || description.value() == null) {
return false;
}
int length = description.value().length();
return length >= min && length <= max;
}
@Override
public Object[] arguments(Description description) {
return new Object[]{String.valueOf(min), String.valueOf(max)};
}
@Override
public @NotNull String defaultMessageFormat() {
return "Description must be between {1} and {2} characters";
}
@Override
public @NotNull String messageKey() {
return "DESCRIPTION_LENGTH_INVALID";
}
}
}
Do you think it would make sense to add such option to the ViolationMessage? I would think we could make it more robust. What do you think?
I still can't grasp what the pain point is.
The ViolationMessage class is the place to define static messages. Think of it as a line in messages.properties.
Messages are resolved in the ConstraintViolation class. It sounds like having the ViolationMessage class hold args and message format would be going beyond its intended role.
My point is, we have an interface Constraint<T, V, C extends Constraint<T, V, C>> that allow the user to overwrite the default message of a given validation but this interface does not allow the user to create messages that depends on parameters. The messages can be only simple messages. Maybe this interface could be expended to accept a SimpleMessageFormat for example or any other strategy that would allow the message to accept parameters. My message with param1 {0} and param2 {1} .
package am.ik.yavi.core;
import am.ik.yavi.core.ViolationMessage.Default;
import am.ik.yavi.jsr305.Nullable;
import java.util.Collection;
import java.util.Deque;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
public interface Constraint<T, V, C extends Constraint<T, V, C>> {
C cast();
default C message(String message) {
ConstraintPredicate<V> predicate = (ConstraintPredicate)this.predicates().pollLast();
if (predicate == null) {
throw new IllegalStateException("no constraint found to override!");
} else {
this.predicates().addLast(predicate.overrideMessage(message));
return this.cast();
}
}
default C message(ViolationMessage message) {
ConstraintPredicate<V> predicate = (ConstraintPredicate)this.predicates().pollLast();
if (predicate == null) {
throw new IllegalStateException("no constraint found to override!");
} else {
this.predicates().addLast(predicate.overrideMessage(message));
return this.cast();
}
}
}
I'm sorry, but I'm having difficulty understanding what you're proposing here.
Perhaps you could provide a more concrete example contrasting the current implementation with your proposed enhancement to help me better understand your use case.
🚀 Enhancement Proposal: Dynamic Message Formatting with Arguments
After further review and considering YAVI's current design principles (minimalism, clarity, and immutability), I'd like to propose a refined and clear approach to solving the issue providing dynamic message formatting with runtime arguments.
🎯 Current Problem:
Currently, the YAVI API supports two methods for customizing validation messages:
- Static messages:
message(String message) - Key-based messages:
message(ViolationMessage message)
Neither of these directly support dynamic formatting using arguments provided at runtime. For example, creating messages like the following isn't straightforward:
"My custom message: \"{0}\" must be between {1} and {2}. Provided value: {3}"
🚩 Why Current Solutions are Insufficient:
-
message(String message):- No support for dynamic formatting.
- No
messageKeysupport, limiting internationalization and message reuse.
-
message(ViolationMessage message):- Supports structured message and key-based lookup.
- But still lacks an easy way to supply custom arguments directly at constraint definition.
✅ Recommended Enhancement (Final Proposal):
To address this, a clean and non-invasive way forward is to introduce an overloaded message method directly within the Constraint interface, allowing dynamic arguments alongside the existing ViolationMessage. This approach maintains full compatibility, preserves the existing design philosophy, and offers robust internationalization support:
🛠️ Proposed Changes:
1. Extend Constraint Interface with a new message overload:
default C message(ViolationMessage violationMessage, Object... args) {
ConstraintPredicate<V> predicate = this.predicates().pollLast();
if (predicate == null) {
throw new IllegalStateException("No constraint found to override!");
}
this.predicates().addLast(predicate.overrideMessage(violationMessage, args));
return this.cast();
}
2. Update ConstraintPredicate to support overriding messages with dynamic arguments:
public ConstraintPredicate<V> overrideMessage(ViolationMessage violationMessage, Object... args) {
return new ConstraintPredicate<>(
this.predicate,
violationMessage.messageKey(),
violationMessage.defaultMessageFormat(),
() -> args, // updated supplier to directly use provided arguments
this.nullAs
);
}
🧑💻 Example Usage:
Here's how users would leverage this new method clearly and intuitively:
Validator<User> validator = validatorFactory.validator(builder -> builder
.constraint(User::getAge, "age", c ->
c.greaterThanOrEqual(18)
.message(ViolationMessage.Default.NUMERIC_GREATER_THAN_OR_EQUAL, "Age", 18))
);
- Resulting Message:
"Age" must be greater than or equal to 18
🚦 Advantages of this Approach:
-
✅ Clear Separation of Concerns:
Violations remain structured around message keys and formatting logic remains explicitly encapsulated. -
✅ Full Internationalization (i18n) Support:
Maintains clear message keys for translation and localization purposes. -
✅ Non-breaking and Minimalistic:
No disruptive changes to existing API users; purely additive enhancement. -
✅ High Flexibility and Clarity:
Provides direct, intuitive formatting for dynamic validation messages. -
✅ Consistency with Existing Design:
Fully aligns with the existing YAVI philosophy and conventions.
⚠️ Important Considerations:
- This enhancement does not alter or complicate the existing
ViolationMessageinterface, maintaining its original simplicity and immutability. - It avoids ambiguity associated with method overloading on the simple
message(String)method by clearly usingViolationMessage.
🔖 Summary Table of the Proposed Change:
| Aspect | Impact |
|---|---|
| API Change | ✅ Additive, non-breaking |
| Complexity | ✅ Minimal increase |
| Internationalization (i18n) | ✅ Fully supported |
| MessageKey Support | ✅ Preserved and enhanced |
| User Flexibility | ✅ Significantly improved |
| Backward Compatibility | ✅ Fully maintained |
🚀 Next Steps:
- Review and agree on this recommended approach.
- Proceed with the implementation in a Pull Request.
- Update relevant documentation and examples clearly illustrating this new capability.
Thank you for considering this enhancement! I'm happy to support with further clarifications or implementation steps.
Please don't rely on AI to write your proposal. Think for yourself and carefully review the AI's answers.
YAVI automatically adds the field name to the beginning of args and the given value to the end. Many default message formats do not reference given values, but you can reference given values by customizing the message format.
https://github.com/making/yavi/blob/04d02b5f437ffe93a348d1c7f609f4077f306b77/src/main/java/am/ik/yavi/core/Validator.java#L383-L398
This example doesn't make sense. This AI's proposal is not dynamic but static.
Validator<User> validator = validatorFactory.validator(builder -> builder .constraint(User::getAge, "age", c -> c.greaterThanOrEqual(18) .message(ViolationMessage.Default.NUMERIC_GREATER_THAN_OR_EQUAL, "Age", 18)) );
This can be replaced statically by
Validator<User> validator = validatorFactory.validator(builder -> builder
.constraint(User::getAge, "age", c ->
c.greaterThanOrEqual(18).message("\"Age\" must be greater than or equal to 18"))
);
and I would write
Validator<User> validator = validatorFactory.validator(builder -> builder
.constraint(User::getAge, "age", c ->
c.greaterThanOrEqual(18).message("\"Age\" must be greater than or equal to {1}"))
);
or
Validator<User> validator = validatorFactory.validator(builder -> builder
.constraint(User::getAge, "Age", c ->
c.greaterThanOrEqual(18).message("\"{0}\" must be greater than or equal to {1}"))
);
If what you want to achieve is to reference an input value in a message, you can already do that.
https://yavi.ik.am/#creating-a-custom-constraint
Hi Maki,
Please, don't take it wrong. I am thinking for myself, but I used the AI to polishing and format a message. I think AI is very useful for brainstorming and this is what I am trying to do with you here. It is totally fine if you disagree in what I am proposing. It least I am making you think of options. Maybe I am taking the wrong approach here by not fully understand the framework in all its complexities and dimensions. AI doesn't replace your or mine technical consideration and evaluation in the end whatsoever.
Anyway, moving foward with the ideia, here are some considerations that I have:
You are relying on a a message ViolationMessage.Default.NUMERIC_GREATER_THAN_OR_EQUAL that is from the framework. I want to rely on my own messageKey and defaultMessage and be able to format it in whatever way I may desire. I know the framework follow a convention but it might be useful to have full control if needed or if convenient as well.
I want to be able to overload the arguments to be used by the downstream by a MessageSource. How do you overload the arguments? At the moment you can't overload the arguments for a framework validation function.
you can overload the arguments from the c.greaterThanOrEqual(18)` function for example or from any other validation message.
Let's say that I use the message key and from the constraint and I want to internacionalize it? how would you approach it?
message.properties
# Validation messages
error1=My error1 message in english with param {0} and {1}
error2=My error2 message in english with param {0} and {1}
public enum MessageCode {
// Common error codes
ERROR1,
ERROR2,
}
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import java.util.Locale;
@Component
public class LocalizedMessages {
private final MessageSource messageSource;
public LocalizedMessages(MessageSource messageSource) {
this.messageSource = messageSource;
}
public String getMessage(MessageCode messageCode, Object... args) {
return messageSource.getMessage(messageCode.name().toLowerCase(), args, Locale.getDefault());
}
public String getMessage(String messageCode, Object... args) {
return messageSource.getMessage(messageCode.toLowerCase(), args, Locale.getDefault());
}
}
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolationsException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.NoSuchMessageException;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class LocalizedConstraintViolationsExceptionResolver {
private final LocalizedMessages localizedMessages;
private final ConstraintViolationsException exception;
public LocalizedConstraintViolationsExceptionResolver(LocalizedMessages localizedMessages,
ConstraintViolationsException exception) {
this.localizedMessages = localizedMessages;
this.exception = exception;
}
public Map<String, String> resolve() {
return exception.violations().stream()
.collect(Collectors.toMap(
violation -> extractSimpleKey(violation.name()), // Field name
this::localizedMessage,
(existing, duplicate) -> existing // Handle duplicate keys by keeping the first value
));
}
private String localizedMessage(ConstraintViolation violation) {
try {
return localizedMessages.getMessage(violation.messageKey().toLowerCase(), violation.args());
} catch (NoSuchMessageException e) {
try {
return localizedMessages.getMessage(violation.message().toLowerCase(), violation.args());
}catch(NoSuchMessageException ex) {
log.info("No message found for key: {}. Update messages.properties to resolve the message with a user friendly message.", violation.messageKey());
return violation.message();
}
}
}
private static String extractSimpleKey(String fullPath) {
// Extract the last part of the field name (e.g., "email" from "email.email")
String[] parts = fullPath.split("\\.");
return parts[parts.length - 1];
}
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
private final LocalizedMessages localizedMessages;
/**
* Constructs a new GlobalExceptionHandler.
*
* @param localizedMessages the localized messages
*/
public GlobalExceptionHandler(LocalizedMessages localizedMessages) {
this.localizedMessages = localizedMessages;
}
/**
* Handles ConstraintViolationsException.
*
* @param ex the exception
* @return the problem detail
*/
@ExceptionHandler(ConstraintViolationsException.class)
public ProblemDetail handleConstraintViolationException(ConstraintViolationsException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
// Build error messages map using Stream API with safeguards
Map<String, String> errorMessages = new LocalizedConstraintViolationsExceptionResolver(localizedMessages, ex).resolve();
problemDetail.setTitle("Validation Error");
problemDetail.setDetail("One or more fields failed validation");
problemDetail.setProperty("violations", errorMessages);
return problemDetail;
}
}
I'm always open. I just wanted to understand user's pain points.
You are relying on a a message ViolationMessage.Default.NUMERIC_GREATER_THAN_OR_EQUAL that is from the framework. I want to rely on my own messageKey and defaultMessage and be able to format it in whatever way I may desire. I know the framework follow a convention but it might be useful to have full control if needed or if convenient as well.
If you want to change the messageKey and defaultMessage, that's a different constraint.
Use CustomConstraint.
public class MyGreaterThan<T extends Number> implements CustomConstraint<T> {
private final Number min;
public MyGreaterThan(Number min) {
this.min = min;
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be greater than {1} but the given value is {2}.";
}
@Override
public String messageKey() {
return MessageCode.ERROR1.name().toLowerCase(Locale.ROOT);
}
@Override
public boolean test(T number) {
if (number == null) {
return true;
}
return number.doubleValue() >= min.doubleValue();
}
@Override
public Object[] arguments(T violatedValue) {
return new Object[] { this.min /* {1} */ };
}
}
Validator<Car> validator = ValidatorBuilder.<Car>of()
.constraint(Car::seatCount, "seatCount", c -> c.predicate(new MyGreaterThan<>(2)))
.build();
ConstraintViolations violations = validator.validate(new Car("aa", "a", 1));
violations.forEach(violation -> System.out.println(violation.message()));
// "seatCount" must be greater than 2 but the given value is 1
Internationalization is a different topic. What did you want to show by presenting LocalizedMessages, LocalizedConstraintViolationsExceptionResolver, and GlobalExceptionHandler?
If you are using Spring Framework, you can use MessageSourceMessageFormatter and you can delegate the internalization mechanism to Spring.
https://yavi.ik.am/#integration-with-messagesource-in-spring-framework
If you want to use the Locale from the user request, you can pass a Locale to the validate method, and it will be used when the ConstraintViolation resolves the message.
https://github.com/making/yavi/blob/04d02b5f437ffe93a348d1c7f609f4077f306b77/src/main/java/am/ik/yavi/core/Validatable.java#L56-L58
Alternatively, in the case of REST, one option would be to just return the key and args and leave resolution to the client side.
Thank you Maki.
I use the CustomConstraint in some places.
Thank for sharing about the MessageSourceMessageFormatter. It might be a way to go foward in my code base.
The way I am using the formatting is not from within the framework itself but rather extracting the information from the violations and resolving the messages extracting the messageKey and the argumets from the violation and generating final internacionalized message to be handled by the GlobalExcepitionHandling using Spring @ControllerAdvice.
I still believe it might be sensible to add an overload for the C message(ViolationMessage violationMessage, Object... args).
Anyway. I believe you can close this issue.
Thank you once again.
Regards,
Flavio Oliva
The way I am using the formatting is not from within the framework itself but rather extracting the information from the
violationsand resolving the messages extracting the messageKey and the argumets from the violation and generating final internacionalized message to be handled by theGlobalExcepitionHandlingusing Spring@ControllerAdvice.
I don't see anything wrong with taking that approach. What I want to understand is whether it has anything to do with the original topic, "want to pass args when defining a message."
I still believe it might be sensible to add an overload for the
C message(ViolationMessage violationMessage, Object... args).
I don't understand from the start why you need to pass args when defining a message, and your example didn't explain it.
I believe that the args should be sufficient for what is passed at runtime (field name, constraint parameters, input values), and if there is additional stuff to add, it should go directly into the message format, not in the args.
Anyway. I believe you can close this issue.
The need for additional args means that I've either overlooked something or you've misunderstood the spec. If I've overlooked something, I'd like to understand it, and if you've misunderstood, I'd like to help you understand that.
Lets exam the interface Constraint<T, V, C extends Constraint<T, V, C>>:
it has:
default C message(String message) {
ConstraintPredicate<V> predicate = this.predicates().pollLast();
if (predicate == null) {
throw new IllegalStateException("no constraint found to override!");
}
this.predicates().addLast(predicate.overrideMessage(message));
return this.cast();
}
default C message(ViolationMessage message) {
ConstraintPredicate<V> predicate = this.predicates().pollLast();
if (predicate == null) {
throw new IllegalStateException("no constraint found to override!");
}
this.predicates().addLast(predicate.overrideMessage(message));
return this.cast();
}
This method manipulates the ConstraintPredicate class.
It polls the last item from the list, change the message of it then puts it back.
Let's look at class ConstraintPredicate<V>:
public class ConstraintPredicate<V> {
private final Supplier<Object[]> args;
private final String defaultMessageFormat;
private final String messageKey;
private final NullAs nullAs;
private final Predicate<V> predicate;
...
}
The ConstraintPredicate have a messageKey and args.
this is the building block for the ConstraintViolation
public class ConstraintViolation {
private final Object[] args;
private final String defaultMessageFormat;
private final Locale locale;
private final MessageFormatter messageFormatter;
private final String messageKey;
private final String name;
....
}
Let say that I have violations in ConstraintViolationsException :
public class ConstraintViolationsException extends RuntimeException {
private final ConstraintViolations violations;
public ConstraintViolationsException(String message,
List<ConstraintViolation> violations) {
super(message);
this.violations = ConstraintViolations.of(violations);
}
public ConstraintViolationsException(List<ConstraintViolation> violations) {
this("Constraint violations found!" + System.lineSeparator()
+ ConstraintViolations.of(violations).violations().stream()
.map(ConstraintViolation::message).map(s -> "* " + s)
.collect(Collectors.joining(System.lineSeparator())),
violations);
}
public ConstraintViolations violations() {
return violations;
}
}
I want to get the ConstraintViolations violations in the @ControllerAdvice and use the messageKey and args to generate the final message. However, I can not manipulate the args from the ConstraintPredicate which is used to generate the ConstraintViolation.
Your explanation is just an explanation of the source code, not why you need to pass args.
Again, the args contains the field name, constraint parameters, and input values, which should be enough.
This information is available in ConstraintViolation#args and should be referenced in the ControllerAdvice.
I want to get the ConstraintViolations violations in the @ControllerAdvice and use the messageKey and args to generate the final message.
You can.
However, I can not manipulate the args from the ConstraintPredicate which is used to generate the ConstraintViolation.
Why do you need to "manipulate" the args? This is what I've been asking all this time.
Why do you need to "manipulate" the args? This is what I've been asking all this time.
For the sake of flexibility.
look for example:
default C notNull() {
this.predicates().add(ConstraintPredicate.of(Objects::isNull, OBJECT_IS_NULL,
() -> new Object[] {}, NullAs.INVALID));
return this.cast();
}
Lets say that I create a validator that call this validation isNull but then deside for whaever reason that I want a fancy message using a fancy message to be internacionalized: "Your object can not be null. Possible values are: {0}"
I will resolve the internacionalization in the GlobalExceptionHandler in the @ControllerAdvice
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Example {
public void validate() {
// Example of using YAVI ValidatorBuilder
Validator<String> validator = new ValidatorBuilder<String>()._string(
s -> s, "string", c -> c.notNull().message("String must not be null. Possible values are: {0}")) // I want to rely on the messageKey to generate the final message upstream
.build();
ConstraintViolations violations = validator.validate("test");
for (var violation : violations) {
System.out.println(violation.messageKey()); //OBJECT_NOT_NULL
System.out.println(violation.message()); // String must not be null. Possible values are: {0}
// I want to format the message in another language upstream using the messageKey and the args but the .notNull() has empty arguments
}
}
}
That sounds more like a violation of a responsibility than flexibility. If you want to give feedback on "possible values", you should use a constraint that checks whether the value is one of the possible values. The possible values would then be included in the constraint's parameters, so you could reference them in the message.
Your example could be rewritten using the built-in constraints as follows:
Validator<Car> validator = ValidatorBuilder.<Car>of()
.constraint(Car::manufacturer, "manufacturer",
c -> c.notNull().oneOf(Set.of("TOYOTA", "HONDA")).message("Possible values are: {1}"))
.build();
ConstraintViolations violations = validator.validate(new Car("aaa", "a", 2));
violations.forEach(violation -> System.out.println(violation.messageKey() + ": " + violation.message()));
// object.oneOf: Possible values are: [TOYOTA, HONDA]
As from my understanding you have 2 constraints chained notNull() and oneOf. My point is: I want to manipulate the messageKey, defaultMessage and args from notNull(). My view is that this is not a violation of responsability. You can manipulate the message and the key but not the args.
I think that as part of the message management I should be able to manage all the three aspect of a message with easy.
The arg is the missing bit in my understanding.
What I'm saying is that args are determined automatically, and shouldn't be set when defining constraints. If you need something other than what's determined automatically, you should be able to build it into the message format. I'd like to see counterexamples, but the examples so far don't convince me. Seems like a misuse to me.
How to build the ConstraintViolation is up to the user to decide. The framework provides its defaults but should allow the user to customise in the any way they intend to.
It is like in many things in life. Things were meant to be used in a given way, but people often find other useful ways to use that.
Let the user decide.
Anyway.
The idea has been planted.
You can think about it and eventually change your mind. For me, I would appreciate the change.
Thank you very much for all the mindful time spent on the discussion. It raised some good / positive valuation.
The framework provides its defaults but should allow the user to customise in the any way they intend to.
I don't think so. Adding options that lead to incorrect usage isn't flexibility - it's a design flaw. Making a non-null field nullable just because "it's the user's intention" to pass null is the wrong decision.
people often find other useful ways to use that.
Yes, I've been asking all along to know whether your use case is actually useful. The examples I've received so far I recognize not as useful, but as misuse. If you could provide more appropriate examples, I might be able to properly understand your intention.
For example, in YAVI 0.15.0, I introduced the builder pattern to ConstraintViolation, increasing the ease of constructing ConstraintViolations (https://github.com/making/yavi/pull/451). Initially, I thought that constructing ConstraintViolation was an internal process and should not be explicitly done by users. However, based on user feedback, I recognized that there are useful use cases where creating ConstraintViolation is convenient. Something like this. So, I changed my thinking.
I'm always open to this, and I have no intention of closing this issue myself, as you or others may provide a valid use case.
Thank you for the reply Maki.
The use case is:
I am applying a validation (any built-in validation) and I want to customize the validation message (defaultMessage, messageKey and args). I don't want to rely on the defaultMessage, messageKey, and args the validation provides by default. I want to overwrite the existing ConstraintViolation attributes allowing me to handle it the way I want. I don't want to create a custom validation with similar rules the framework already provides to overwrite a validation defaultMessage, messageKey and args. I use the messageKey and args to extract a message from the messageSource format the message upstream. I do this because the The validator is a bean, a singleton, and when I create them they don't have the Locale. When I create the Validators I don't resolve the message formatter. I delegate the resolution of message formatting in a separate component in a different moment.
Since messageKey and defaultMessage can currently be customized via ViolationMessage, the discussion narrows down to just args. Your explanation still doesn't include any necessity for customizing args. You're simply saying "I want to customize it." I've consistently maintained that there's no need to change args, so our discussion remains at an impasse.
When I create the Validators I don't resolve the message formatter.
I tend to agree with this point.
Having to decide on the MessageFormatter when defining the Validator is inconvenient when you want to obtain the MessageFormatter using DI.
I think a method like the following should be added so that the MessageFormatter can be passed when resolving the message.
public String message(MessageFormatter messageFormatter) {
return messageFormatter.format(this.messageKey, this.defaultMessageFormat, this.args, this.locale);
}
I think this method can be useful but it still happens to need the args from the ConstraintViolation.
For example the notNull() violation. it has the args as the attribute name .
Let say that I want to customize the message providing a list of possible values of the notNull validation and I want to do this using the messageKey and the args[].
The message:
The field {0} should not be null. Possible values are: {1}
This template message will not be resolved correctly using the values from notNull() validation.
The oneOf validator can be used to address this validation message above. but it still doesnt sove a more elaborated message.
My elaborated message with multiple parameters: {0}, {1}, {2}
Look at this example:
Even if you dont allow for the builtin validators to accept args the .predicate() constraint would make sense to have that.
Look at the example:
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;
import am.ik.yavi.core.ViolationMessage;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ValidatorTest {
public record Value(String value) {
}
@Test
void test() {
// Given
Validator<Value> validator = ValidatorBuilder.<Value>of()
._string(s -> s.value, "value", c -> c.oneOf(List.of("A","B")).message(ViolationMessage.of("notNull", "{0} must not be null. Possible values are: {1}")))
._string(s -> s.value, "value", c -> c.oneOf(List.of("A","B")).message(ViolationMessage.of("notNull", "My elaborated message with multiple parameters: {0}, {1}, {2}, {3}")))
._string(s -> s.value, "value", c -> c.predicate(s -> s.equals("A"), "myPredicateKey", "My elaborated message with multiple parameters: {0}, {1}, {2}, {3}")) // here the predicate does not allow to resolve the args as well
._string(s -> s.value, "value", c -> c.predicate(s -> s.equals("A"), ViolationMessage.of("myPredicateKey", "My elaborated message with multiple parameters: {0}, {1}, {2}, {3}"))) // here the predicate does not allow to resolve the args as well
._string(s -> s.value, "value", c -> c. predicate(s -> s.equals("A"), ViolationMessage.of("myPredicateKey", "My elaborated message with multiple parameters: {0}, {1}, {2}, {3}"))) // here the predicate does not allow to resolve the args as well
.build();
var validate = validator.validate(new Value("C"));
assertThat(validate.isValid()).isFalse();
assertThat(validate).hasSize(5);
for (int i = 0; i < validate.size(); i++) {
System.out.println(validate.get(i));
}
}
}
Output:
ConstraintViolation{name='value', messageKey='notNull', defaultMessageFormat='{0} must not be null. Possible values are: {1}', args=[value, [A, B], C]}
ConstraintViolation{name='value', messageKey='notNull', defaultMessageFormat='My elaborated message with multiple parameters: {0}, {1}, {2}, {3}', args=[value, [A, B], C]}
ConstraintViolation{name='value', messageKey='myPredicateKey', defaultMessageFormat='My elaborated message with multiple parameters: {0}, {1}, {2}, {3}', args=[value, C]}
ConstraintViolation{name='value', messageKey='myPredicateKey', defaultMessageFormat='My elaborated message with multiple parameters: {0}, {1}, {2}, {3}', args=[value, C]}
ConstraintViolation{name='value', messageKey='myPredicateKey', defaultMessageFormat='My elaborated message with multiple parameters: {0}, {1}, {2}, {3}', args=[value, C]}
As you can see not allowing to manage the args does not allow the user to fully customize the message from the predicate. the only way to fully customize it is by the CustomConstraint.
I believe it would make sense to allow the user to provide the args at least in the predicate method. The exact way i am not entirely sure.
It seems the same argument is repeated. The use case is the abuse of args. The args consists of the field name, constraint parameters, and violation value, and is determined dynamically at runtime, not at definition time. Anything else is essentially unnecessary, and the requirement of it suggests something is wrong. 'possible values' should be a constraint parameter. Your violation message goes beyond the responsibilities of a 'not null' constraint. You should use a 'one of' constraint or a custom constraint where 'possible values' is a constraint parameter. If you insist that it is not a constraint parameter, then that falls into the category of something that should be hard-coded into the message format. The reason you can customize arguments with CustomConstraint is not because it is flexible, but because it is the only way to pass constraint parameters to args.