8000 add validation to description based on the validation annotations by edewit · Pull Request #47375 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

add validation to description based on the validation annotations - #47375

Merged
vmuzikar merged 21 commits into
keycloak:mainfrom
edewit:issue-47371
Apr 29, 2026
Merged

vmuzikar merged 21 commits into
keycloak:mainfrom
edewit:issue-47371

Conversation

@edewit
@edewit edewit commented Mar 23, 2026
Copy link
Copy Markdown
Contributor

fixes: #47371
Signed-off-by: Erik Jan de Wit erikjan.dewit@gmail.com

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ValidationAnnotationScanner to read validation annotations (field, type-use, and select class-level) and translate them into OpenAPI schema properties and/or description text.
  • Updates OASModelFilter to invoke the scanner while populating schema property descriptions, appending a Validation: ... 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.

Comment on lines +78 to +81
// @NotEmpty implies minItems: 1 for arrays/collections
AnnotationInstance notEmpty = getFieldAnnotation(field, NOT_EMPTY);
if (notEmpty != null && propertySchema.getMinItems() == null) {
propertySchema.setMinItems(1);
Copilot AI Mar 23, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Suggested change
// @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);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +168
// Check for @URL
AnnotationInstance url = getFieldAnnotation(field, URL);
if (url != null) {
constraints.add("must be a valid URL");
}
Copilot AI Mar 23, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
// @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);
}
Copilot AI Mar 23, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@michalvavrik michalvavrik left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely sure what you mean by that, but added this error to the scanner in this commit 8bfc3c3

@michalvavrik michalvavrik Mar 25, 2026 •
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely sure what you mean by that, but added this error to the scanner in this commit 8bfc3c3

8bfc3c3 is exactly what I meant. it wasn't there when I reviewed. Thanks

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");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what do you prefer constants over DotName.createSimple(Valid.class)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an opinion about it the class overload is new could change it to that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's just my preference, I'll leave it to you to decide.

@michalvavrik michalvavrik left a comment •
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works for me; the only thing I am surprised is that if these changes OpenApi descriptions, I think you didn't build the whole project as if you did, the OpenAPI stored in JS dir would change and we could see clearly effect of these changes.

@mabartos
Copy link
Copy Markdown
Member

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.

@edewit
edewit requested a review from a team as a code owner March 25, 2026 10:21
@edewit
edewit commented Mar 25, 2026 •
Copy link
Copy Markdown
Contributor Author

Right @mabartos that's why I removed my custom schema property logic for standard Jakarta Validation annotations (@notblank, @notempty, @SiZe, @pattern, @min, @max) -
• Kept @url → format: uri handling (SmallRye doesn't support Hibernate Validator's @url)
• Kept all human-readable description building (SmallRye doesn't do this)

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If 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#testExceedMaxTemporaryLockouts

Keycloak CI - Forms IT (chrome)

java.lang.AssertionError: Expected LoginPage but was localhost (https://localhost:8543/auth/realms/test/login-actions/authenticate?session_code=ZEaHEttdxxO2_1sbCYSu_G8ORCbChBBg8VNB7IBedbw&execution=1750cf17-4ff5-4b9d-ac9b-234632b7b5d8&client_id=test-app&tab_id=UmiXYvUq1JA&client_data=eyJydSI6Imh0dHBzOi8vbG9jYWxob3N0Ojg1NDMvYXV0aC9yZWFsbXMvbWFzdGVyL2FwcC9hdXRoIiwicnQiOiJjb2RlIn0)
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:39)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
...

Report flaky test

@vmuzikar
Copy link
Copy Markdown
Contributor

@shawkins @Pepo48 @mabartos Can you please (re-)review as well?

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment on lines +141 to +154
// 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);
}
}
Copilot AI Mar 26, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@mabartos
Copy link
Copy Markdown
Member

Right @mabartos that's why I removed my custom schema property logic for standard Jakarta Validation annotations (@notblank, @notempty, @SiZe, @pattern, @min, @max)

@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

@edewit
edewit commented Mar 26, 2026
Copy link
Copy Markdown
Contributor Author

@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 BeanValidationScanner. But I've kept the human-readable descriptions (e.g., "must not be blank"). That was part of the issue to have this!

edewit and others added 21 commits April 29, 2026 13:56
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>
@vmuzikar
vmuzikar merged commit ccacca5 into keycloak:main Apr 29, 2026
90 checks passed
@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If 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#testEmptyBaseUrl

Keycloak CI - Forms IT (chrome)

org.opentest4j.AssertionFailedError: Expected LoginPage but was localhost (https://localhost:8543/auth/realms/test/protocol/openid-connect/auth?scope=openid&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A8543%2Fauth%2Frealms%2Fmaster%2Fapp%2Fauth&client_id=empty-baseurl-client) ==> expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@stianst stianst mentioned this pull request Jun 1, 2026
@edewit
edewit deleted the issue-47371 branch June 11, 2026 09:03
sashyo pushed a commit to tide-foundation/keycloak-IGA that referenced this pull request Jul 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose validations in OpenAPI spec for Client v2

7 participants

0