-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhance decimal validation to match actual Jakarta Validation behavior and fix related issues #1131
base: main
Are you sure you want to change the base?
Changes from all commits
2dcd754
396ef8f
9598de3
952364b
eb987a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -292,178 +292,126 @@ public JavaIntegerConstraint generateIntegerConstraint(ArbitraryGeneratorContext | |
@Override | ||
@Nullable | ||
public JavaDecimalConstraint generateDecimalConstraint(ArbitraryGeneratorContext context) { | ||
BigDecimal positiveMin = null; | ||
Boolean positiveMinInclusive = null; | ||
BigDecimal positiveMax = null; | ||
Boolean positiveMaxInclusive = null; | ||
BigDecimal negativeMin = null; | ||
Boolean negativeMinInclusive = null; | ||
BigDecimal negativeMax = null; | ||
boolean negativeMaxInclusive = false; | ||
BigDecimal min = null; | ||
Boolean minInclusive = null; | ||
BigDecimal max = null; | ||
Boolean maxInclusive = null; | ||
Integer scale = null; | ||
|
||
Optional<Digits> digits = context.findAnnotation(Digits.class); | ||
if (digits.isPresent()) { | ||
BigDecimal value = BigDecimal.ONE; | ||
int integer = digits.get().integer(); | ||
if (integer > 1) { | ||
value = BigDecimal.TEN.pow(integer - 1); | ||
for (Min minAnn : context.findAnnotations(Min.class)) { | ||
BigDecimal newMin = BigDecimal.valueOf(minAnn.value()); | ||
if (min == null || newMin.compareTo(min) > 0) { | ||
min = newMin; | ||
minInclusive = true; | ||
} | ||
positiveMax = value.multiply(BigDecimal.TEN).subtract(BigDecimal.ONE); | ||
positiveMin = value; | ||
negativeMax = positiveMin.negate(); | ||
negativeMin = positiveMax.negate(); | ||
positiveMinInclusive = false; | ||
negativeMinInclusive = false; | ||
scale = digits.get().fraction(); | ||
} | ||
|
||
Optional<Min> minAnnotation = context.findAnnotation(Min.class); | ||
if (minAnnotation.isPresent()) { | ||
BigDecimal minValue = minAnnotation.map(Min::value).map(BigDecimal::valueOf).get(); | ||
if (minValue.compareTo(BigDecimal.ZERO) >= 0) { | ||
if (positiveMin == null) { | ||
positiveMin = minValue; | ||
} else { | ||
positiveMin = positiveMin.min(minValue); | ||
} | ||
negativeMax = null; | ||
negativeMin = null; | ||
} else { | ||
if (negativeMin == null) { | ||
negativeMin = minValue; | ||
} else { | ||
negativeMin = negativeMin.min(minValue); | ||
} | ||
negativeMinInclusive = true; | ||
for (DecimalMin decimalMin : context.findAnnotations(DecimalMin.class)) { | ||
BigDecimal newMin = new BigDecimal(decimalMin.value()); | ||
if (min == null || newMin.compareTo(min) > 0 | ||
|| (newMin.compareTo(min) == 0 && !decimalMin.inclusive() && minInclusive)) { | ||
min = newMin; | ||
minInclusive = decimalMin.inclusive(); | ||
} | ||
} | ||
|
||
Optional<DecimalMin> decimalMinAnnotation = context.findAnnotation(DecimalMin.class); | ||
if (decimalMinAnnotation.isPresent()) { | ||
BigDecimal decimalMin = new BigDecimal( | ||
decimalMinAnnotation | ||
.get() | ||
.value() | ||
); | ||
|
||
if (decimalMin.compareTo(BigDecimal.ZERO) >= 0) { | ||
if (positiveMin == null) { | ||
positiveMin = decimalMin; | ||
} else { | ||
positiveMin = positiveMin.min(decimalMin); | ||
} | ||
if (!decimalMinAnnotation.map(DecimalMin::inclusive).get()) { | ||
positiveMinInclusive = false; | ||
} | ||
negativeMax = null; | ||
negativeMin = null; | ||
} else { | ||
if (negativeMin == null) { | ||
negativeMin = decimalMin; | ||
} else { | ||
negativeMin = negativeMin.min(negativeMin); | ||
} | ||
if (!decimalMinAnnotation.map(DecimalMin::inclusive).get()) { | ||
negativeMinInclusive = false; | ||
} | ||
for (Max maxAnn : context.findAnnotations(Max.class)) { | ||
BigDecimal newMax = BigDecimal.valueOf(maxAnn.value()); | ||
if (max == null || newMax.compareTo(max) < 0) { | ||
max = newMax; | ||
maxInclusive = true; | ||
} | ||
} | ||
|
||
Optional<Max> maxAnnotation = context.findAnnotation(Max.class); | ||
if (maxAnnotation.isPresent()) { | ||
BigDecimal maxValue = maxAnnotation.map(Max::value).map(BigDecimal::valueOf).get(); | ||
if (maxValue.compareTo(BigDecimal.ZERO) > 0) { | ||
if (positiveMax == null) { | ||
positiveMax = maxValue; | ||
} else { | ||
positiveMax = positiveMax.max(maxValue); | ||
} | ||
} else { | ||
if (negativeMax == null) { | ||
negativeMax = maxValue; | ||
} else { | ||
negativeMax = negativeMax.max(maxValue); | ||
} | ||
for (DecimalMax decimalMax : context.findAnnotations(DecimalMax.class)) { | ||
BigDecimal newMax = new BigDecimal(decimalMax.value()); | ||
if (max == null || newMax.compareTo(max) < 0 | ||
|| (newMax.compareTo(max) == 0 && !decimalMax.inclusive() && maxInclusive)) { | ||
max = newMax; | ||
maxInclusive = decimalMax.inclusive(); | ||
} | ||
} | ||
|
||
Optional<DecimalMax> decimalMaxAnnotation = context.findAnnotation(DecimalMax.class); | ||
if (decimalMaxAnnotation.isPresent()) { | ||
BigDecimal decimalMax = new BigDecimal( | ||
decimalMaxAnnotation | ||
.get() | ||
.value() | ||
); | ||
|
||
if (decimalMax.compareTo(BigDecimal.ZERO) > 0) { | ||
if (positiveMax == null) { | ||
positiveMax = decimalMax; | ||
} else { | ||
positiveMax = positiveMax.max(decimalMax); | ||
} | ||
positiveMaxInclusive = decimalMaxAnnotation.map(DecimalMax::inclusive).get(); | ||
} else { | ||
if (negativeMax == null) { | ||
negativeMax = decimalMax; | ||
} else { | ||
negativeMax = negativeMax.max(decimalMax); | ||
} | ||
negativeMaxInclusive = decimalMaxAnnotation.map(DecimalMax::inclusive).get(); | ||
} | ||
|
||
if (!decimalMaxAnnotation.map(DecimalMax::inclusive).get()) { | ||
positiveMaxInclusive = false; | ||
if (context.findAnnotation(Positive.class).isPresent()) { | ||
if (min == null || BigDecimal.ZERO.compareTo(min) > 0 | ||
|| (BigDecimal.ZERO.compareTo(min) == 0 && minInclusive)) { | ||
min = BigDecimal.ZERO; | ||
minInclusive = false; | ||
} | ||
} | ||
|
||
if (positiveMax == null) { | ||
positiveMax = decimalMax; | ||
} else if (positiveMax.compareTo(decimalMax) > 0) { | ||
positiveMax = decimalMax; | ||
if (context.findAnnotation(PositiveOrZero.class).isPresent()) { | ||
if (min == null || BigDecimal.ZERO.compareTo(min) > 0) { | ||
min = BigDecimal.ZERO; | ||
minInclusive = true; | ||
} | ||
} | ||
|
||
if (context.findAnnotation(Negative.class).isPresent()) { | ||
if (negativeMax == null || negativeMax.compareTo(BigDecimal.ZERO) > 0) { | ||
negativeMax = BigDecimal.ZERO; | ||
negativeMaxInclusive = false; | ||
if (max == null || BigDecimal.ZERO.compareTo(max) < 0 | ||
|| (BigDecimal.ZERO.compareTo(max) == 0 && maxInclusive)) { | ||
max = BigDecimal.ZERO; | ||
maxInclusive = false; | ||
} | ||
} | ||
|
||
if (context.findAnnotation(NegativeOrZero.class).isPresent()) { | ||
if (negativeMax == null || negativeMax.compareTo(BigDecimal.ZERO) > 0) { | ||
negativeMax = BigDecimal.ZERO; | ||
negativeMaxInclusive = true; | ||
if (max == null || BigDecimal.ZERO.compareTo(max) < 0) { | ||
max = BigDecimal.ZERO; | ||
maxInclusive = true; | ||
} | ||
} | ||
|
||
if (context.findAnnotation(Positive.class).isPresent()) { | ||
if (positiveMin == null || positiveMin.compareTo(BigDecimal.ZERO) < 0) { | ||
positiveMin = BigDecimal.ZERO; | ||
positiveMinInclusive = false; | ||
Optional<Digits> digitsAnn = context.findAnnotation(Digits.class); | ||
if (digitsAnn.isPresent()) { | ||
Digits digits = digitsAnn.get(); | ||
int integerDigits = digits.integer(); | ||
int fractionDigits = digits.fraction(); | ||
|
||
StringBuilder maxBuilder = new StringBuilder(); | ||
for (int i = 0; i < integerDigits; i++) { | ||
maxBuilder.append('9'); | ||
} | ||
} | ||
if (fractionDigits > 0) { | ||
maxBuilder.append('.'); | ||
for (int i = 0; i < fractionDigits; i++) { | ||
maxBuilder.append('9'); | ||
} | ||
} | ||
BigDecimal digitsMax = new BigDecimal(maxBuilder.toString()); | ||
BigDecimal digitsMin = digitsMax.negate(); | ||
|
||
if (context.findAnnotation(PositiveOrZero.class).isPresent()) { | ||
if (positiveMin == null || positiveMin.compareTo(BigDecimal.ZERO) < 0) { | ||
positiveMin = BigDecimal.ZERO; | ||
positiveMinInclusive = true; | ||
if (max == null || digitsMax.compareTo(max) < 0) { | ||
max = digitsMax; | ||
maxInclusive = true; | ||
} | ||
if (min == null || digitsMin.compareTo(min) > 0) { | ||
min = digitsMin; | ||
minInclusive = true; | ||
} | ||
|
||
scale = digits.fraction(); | ||
} | ||
|
||
if (positiveMin == null && positiveMax == null && negativeMin == null && negativeMax == null && scale == null) { | ||
if (min == null && max == null) { | ||
return null; | ||
} | ||
|
||
boolean isPositiveMin = min != null && min.compareTo(BigDecimal.ZERO) >= 0; | ||
boolean isPositiveMax = max != null && max.compareTo(BigDecimal.ZERO) >= 0; | ||
boolean isNegativeMin = min != null && min.compareTo(BigDecimal.ZERO) < 0; | ||
boolean isNegativeMax = max != null && max.compareTo(BigDecimal.ZERO) < 0; | ||
|
||
return new JavaDecimalConstraint( | ||
positiveMin, | ||
positiveMinInclusive, | ||
positiveMax, | ||
positiveMaxInclusive, | ||
negativeMin, | ||
negativeMinInclusive, | ||
negativeMax, | ||
negativeMaxInclusive, | ||
isPositiveMin ? min : null, | ||
isPositiveMin ? minInclusive : null, | ||
isPositiveMax ? max : null, | ||
isPositiveMax ? maxInclusive : null, | ||
|
||
isNegativeMin ? min : null, | ||
isNegativeMin ? minInclusive : null, | ||
isNegativeMax ? max : null, | ||
isNegativeMax ? maxInclusive : null, | ||
scale | ||
); | ||
Comment on lines
405
to
416
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why the JavaDecimalConstraint class needs to manage separate min/max values for negative and positive numbers. In the previous implementation with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a better idea for addressing both positive and negative values in the case of For example, in the case of
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at your example with Moreover, simplifying to a single range would actually help prevent potential distribution issues. For instance, with the current implementation, when we have constraints like: @Digits(integer=3, fraction=2)
@Min(-2)
@Max(100)
private BigDecimal b; The |
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not for common case.
You can not confirm that user uses the annotation
@Repeatable
, the name of propertyvalue
.I think it should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongahjo
Ah, I understand! Instead of defining a new
findAnnotations
inArbitraryGeneratorContext
, should I handle duplicate annotations withinArbitraryGeneratorContext
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does
this.getResolvedProperty().getAnnotations()
not return the duplicate annotations?Could you make a test to show when the duplicate annotation is an issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Duplicate annotations appear to be automatically processed in the same way as the annotations in field c.
Therefore, I implemented
findAnnotations
to process the contents withinMax$List
in the same way as regular duplicate annotations, as shown above.Without the
Repeatable
processing infindAnnotations
, if we only usethis.getResolvedProperty().getAnnotations()
, we would need to search twice usinggetAnnotations(Max.class)
andgetAnnotations(Max.List.class)
. This is because we need to consider both cases where there is a single annotation and multiple annotations.Since
getAnnotationsByType
cannot be used in the current implementation, I chose to implementfindAnnotations
to handle both single annotations and container annotations in a unified way.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear to me how it works when there are multiple constraints on the same property.
Does the JSR-303 support multiple annotations on the same property within the same group?
I only found the applying multiple constraints within the different group.
https://beanvalidation.org/1.0/spec/#constraintsdefinitionimplementation-multipleconstraints
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the
@Repeatable
annotation allows multiple annotations to be applied, it was confirmed during testing that all validation annotations (including duplicates) are processed as expected.However, JSR-303 does not define a specific approach for handling duplicate annotations, meaning implementations can vary.
Given this, rather than implementing complex logic to handle duplicates, it may be more practical to revert to the original approach of simply checking for the presence of annotations.