Repository navigation
add validation to description based on the validation annotations - #47375
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the Admin Client v2 OpenAPI generation so that Jakarta Validation (and a couple of Keycloak custom constraints) are reflected in the published schema, primarily by appending constraint information to field descriptions and additionally by setting some machine-readable schema constraints.
Changes:
- Introduces
ValidationAnnotationScannerto read validation annotations (field, type-use, and select class-level) and translate them into OpenAPI schema properties and/or description text. - Updates
OASModelFilterto invoke the scanner while populating schema property descriptions, appending aValidation: ...suffix and setting basic schema constraints (e.g.,minLength,pattern,minimum).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| rest/admin-v2/rest/src/main/java/org/keycloak/admin/internal/openapi/ValidationAnnotationScanner.java | New scanner that inspects Jandex metadata for validation annotations and produces schema property updates + human-readable validation text. |
| rest/admin-v2/rest/src/main/java/org/keycloak/admin/internal/openapi/OASModelFilter.java | Integrates the scann 8000 er into OpenAPI model filtering, combining field-level and class-level validations into property descriptions and applying schema constraints. |
| // @NotEmpty implies minItems: 1 for arrays/collections | ||
| AnnotationInstance notEmpty = getFieldAnnotation(field, NOT_EMPTY); | ||
| if (notEmpty != null && propertySchema.getMinItems() == null) { | ||
| propertySchema.setMinItems(1); |
There was a problem hiding this comment.
@NotEmpty is translated to minItems=1 unconditionally, but @NotEmpty can also apply to String/CharSequence where the equivalent OpenAPI constraint is minLength=1. Please branch based on the schema/field type (string vs array/collection vs map) to avoid emitting invalid or misleading OpenAPI constraints.
| // @NotEmpty implies minItems: 1 for arrays/collections | |
| AnnotationInstance notEmpty = getFieldAnnotation(field, NOT_EMPTY); | |
| if (notEmpty != null && propertySchema.getMinItems() == null) { | |
| propertySchema.setMinItems(1); | |
| // @NotEmpty: behavior depends on schema type | |
| AnnotationInstance notEmpty = getFieldAnnotation(field, NOT_EMPTY); | |
| if (notEmpty != null) { | |
| Schema.SchemaType schemaType = propertySchema.getType(); | |
| if (schemaType == Schema.SchemaType.STRING) { | |
| if (propertySchema.getMinLength() == null) { | |
| propertySchema.setMinLength(1); | |
| } | |
| } else if (schemaType == Schema.SchemaType.ARRAY) { | |
| if (propertySchema.getMinItems() == null) { | |
| propertySchema.setMinItems(1); | |
| } | |
| } else if (schemaType == Schema.SchemaType.OBJECT) { | |
| if (propertySchema.getMinProperties() == null) { | |
| propertySchema.setMinProperties(1); | |
| } | |
| } |
| // Check for @URL | ||
| AnnotationInstance url = getFieldAnnotation(field, URL); | ||
| if (url != null) { | ||
| constraints.add("must be a valid URL"); | ||
| } |
There was a problem hiding this comment.
buildDescription claims to include validation group context, but group context is only prepended for @NotBlank/@NotNull/@NotEmpty (and class-level constraints). For consistency (and to match the Javadoc), either prepend getGroupContext(...) for the other supported constraints too (@URL, @Size, @Pattern, @Min, @Max, @Valid), or adjust the documentation to reflect the current behavior.
| // @NotBlank implies minLength: 1 for strings (only for field-level, not type args) | ||
| AnnotationInstance notBlank = getFieldAnnotation(field, NOT_BLANK); | ||
| if (notBlank != null && propertySchema.getMinLength() == null) { | ||
| propertySchema.setMinLength(1); | ||
| } |
There was a problem hiding this comment.
applySchemaProperties applies machine-readable constraints (e.g., minLength) even when the underlying validation annotation is restricted to specific validation groups. This can make the schema overly strict for operations where the constraint does not apply (e.g., BaseClientRepresentation.clientId is @NotBlank(groups = CreateClient.class) but would still get minLength=1 globally). Consider only applying machine-readable constraints when groups is empty/default, and rely on the human-readable description for group-scoped constraints (or otherwise encode the group scope without tightening the base schema).
There was a problem hiding this comment.
I think it would be nice to have this tested. I realize that if something changes (like field is removed or named) then tests would break, but I'd really prefer if for each validation constraint we already use, there was one description test, e.g. added to OpenApiDistTest. Or if you prefer, you can create a unit test and test ValidationAnnotationScanner in isolation with manually created index and test classes.
| } | ||
|
|
||
| private String getOperationContext(String groupSimpleName) { | ||
| for (Map.Entry<String, String> entry : OPERATION_PREFIXES.entrySet()) { |
There was a problem hiding this comment.
Do I understand it right that at this place, you cannot reference org.keycloak.representations.admin.v2.validation.PutClient, org.keycloak.representations.admin.v2.validation.PatchClient and org.keycloak.representations.admin.v2.validation.CreateClient directly or are you trying to make this generic?
I don't really have hard opinion on this, but personally, if I were writing it and I could reference org.keycloak.representations.admin.v2.validation, I'd just hardcode known groups and fail build if we hit unknown options. Thus whoever adds a new group, would see failing tests and link group to description like "on patch".
But it is just opinion, if you like that, fine.
There was a problem hiding this comment.
I'm not completely sure what you mean by that, but added this error to the scanner in this commit 8bfc3c3
| private static final DotName PATTERN = DotName.createSimple("jakarta.validation.constraints.Pattern"); | ||
| private static final DotName MIN = DotName.createSimple("jakarta.validation.constraints.Min"); | ||
| private static final DotName MAX = DotName.createSimple("jakarta.validation.constraints.Max"); | ||
| private static final DotName VALID = DotName.createSimple("jakarta.validation.Valid"); |
There was a problem hiding this comment.
Can you clarify what do you prefer constants over DotName.createSimple(Valid.class)?
There was a problem hiding this comment.
I don't have an opinion about it the class overload is new could change it to that
There was a problem hiding this comment.
that's just my preference, I'll leave it to you to decide.
|
There's already this logic on the SmallRye side - https://github.com/smallrye/smallrye-open-api/blob/main/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/BeanValidationScanner.java#L32 It'd be good if we could leverage it. I'd like to avoid any maintenance of this custom code. |
|
Right @mabartos that's why I removed my custom schema property logic for standard Jakarta Validation annotations (@notblank, @notempty, @SiZe, @pattern, @min, @max) - |
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.forms.BruteForceTest#testExceedMaxTemporaryLockoutsKeycloak CI - Forms IT (chrome) |
| // Check for @URL | ||
| AnnotationInstance url = getFieldAnnotation(field, URL); | ||
| if (url != null) { | ||
| constraints.add("must be a valid URL"); | ||
| } | ||
|
|
||
| // Check for @Size | ||
| AnnotationInstance size = getFieldAnnotation(field, SIZE); | ||
| if (size != null) { | ||
| String sizeDesc = buildSizeDescription(size); | ||
| if (sizeDesc != null) { | ||
| constraints.add(sizeDesc); | ||
| } | ||
| } |
There was a problem hiding this comment.
buildDescription adds validation group context only for @NotBlank/@NotNull/@NotEmpty, but not for other constraints like @Size, @Pattern, @Min/@Max, @URL, etc. This makes group-scoped constraints for those annotations lose their "on create/update/patch" context and also contradicts the method Javadoc. Consider prefixing every constraint description with getGroupContext(annotation) consistently.
@edewit I don't understand. Did you remove even more custom logic for the annotations? Because I still see these are still handled in the custom code: https://github.com/keycloak/keycloak/pull/47375/changes#diff-b665b37f69099ad25bfc076ec3551293d93f67e7b724a41a7b374fa29b4aedb8R166 Have you checked if could reuse the https://github.com/smallrye/smallrye-open-api/blob/main/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/BeanValidationScanner.java#L32? Thanks |
|
@mabartos yes, like I said I removed it, it no longer sets the constraints (minLength, maxLength, pattern, minimum, etc.) that's done by SmallRye's |
fixes: keycloak#47371 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
SmallRye handles this Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
kept special processing for constraints that have parameters Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Jandex returns null for annotation values that use the default, it only returns values that were explicitly specified. Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Václav Muzikář <vmuzikar@ibm.com>
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.forms.MultipleTabsLoginTest#testEmptyBaseUrlKeycloak CI - Forms IT (chrome) |
…ycloak#47375) * add validation to description based on the validation annotations fixes: keycloak#47371 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added test and better encapsulation Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * Remove applySchemaProperties() from ValidationAnnotationScanner SmallRye handles this Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * change to use class instead of string Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added error when encountering group annotation that isn't in the map Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed test and added openapi.yaml from js folder Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * make it spotless Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * made the scanner more generic and use the validation message kept special processing for constraints that have parameters Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * use interpolation for the other annotations Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * updated more PR reviews Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * more PR review comments Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed merge Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * moved class level validator descriptions Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added field name to annotation, that we can then use in the schema Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * don't put validations that have an affectedField on the class Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * jandex default Jandex returns null for annotation values that use the default, it only returns values that were explicitly specified. Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed test Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * even more PR review comments Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * small fix to get more validations in the description Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * better handle on parmeterised types Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * Update generated spec Signed-off-by: Václav Muzikář <vmuzikar@ibm.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Václav Muzikář <vmuzikar@ibm.com> Co-authored-by: Václav Muzikář <vmuzikar@ibm.com>
fixes: #47371
Signed-off-by: Erik Jan de Wit erikjan.dewit@gmail.com