8000 45364 Migrate to adminClient for Java by robson90 · Pull Request #46973 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

45364 Migrate to adminClient for Java - #46973

Merged
vmuzikar merged 6 commits into
keycloak:mainfrom
robson90:is-45364
Mar 27, 2026
Merged

vmuzikar merged 6 commits into
keycloak:mainfrom
robson90:is-45364

Conversation

@robson90
@robson90 robson90 commented Mar 9, 2026 •
Copy link
Copy Markdown
Contributor

Closes #45364

As requested here are the changes.

Please take a look and give feedback: @vmuzikar & @mabartos

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 migrates the Keycloak Client API v2 tests from raw HTTP client calls (HttpGet, HttpPut, HttpPost, HttpDelete) to using the Java admin client (Keycloak class). It also extracts AdminRootV2 from a concrete class into an interface with a DefaultAdminRootV2 implementation, and introduces a Clients wrapper class that bridges the legacy admin client with the new v2 API.

Changes:

  • Extracted AdminRootV2 into an interface and created DefaultAdminRootV2 as the server-side implementation, enabling client-side proxy usage.
  • Added Clients wrapper class and Keycloak.clients(realmName) method to provide access to both legacy client operations and the new v2 API through the admin client.
  • Migrated most test methods in ClientApiV2Test from raw HTTP calls to use adminClient.clients(...).v2().

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminRootV2.java Refactored from concrete class to interface
rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/DefaultAdminRootV2.java New implementation of AdminRootV2 interface
integration/admin-client/src/main/java/org/keycloak/admin/client/wrapper/Clients.java New wrapper bridging legacy and v2 client APIs
integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java Added clients(realmName) entry point
integration/admin-client/pom.xml Added keycloak-admin-v2-rest dependency
rest/admin-v2/tests/pom.xml Changed dependency to keycloak-admin-client-tests
rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/AbstractClientApiV2Test.java Added realm-parameterized getClientsApiUrl overload
rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java Migrated tests from raw HTTP to admin client

You can also share your feedback on Copilot code review. Take the survey.

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

@robson90 Thank you for the PR. I think it's a good start, we can migrate rest of the tests in a follow-up.

Added some comments. Copilot comments also seem relevant, so could you please review them as well?

setAuthHeader(getRequest);

// TODO this needs to be migrated to the adminClient, when we are able to construct Stream<BaseClientRepresentation>. Jackson config needs to be andjusted.
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.

Stream will need to remain on the endpoint due to performance reasons. Maybe we could use InputStream there instead and make resteasy work with that?

CC @mabartos @shawkins

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.

From my perspective the solution would be something like:
https://github.com/robson90/keycloak/blob/ef6cc6e18425ea1122dd235321f552cea8f7da3b/integration/admin-client/src/main/java/org/keycloak/admin/client/deserializer/StreamDeserializer.java#L14-L55

But as far as I understand from reading through docs of Jackson, Resteasy and Quarkus, you probably want to use something like this: https://quarkus.io/guides/rest#streaming-support

When debugging the failing test, I saw that the ResteasyProvider is invoking some synchronous invoke method for calling Keycloak. Probably that needs to be changed/configured on the Client-Level.

Please let me know what you think about the workaround with the StreamDeserializer.

CC: @lpradel

< 8000 div class="flex-shrink-0 mr-1"> @vmuzikar
vmuzikar requested a review from Copilot March 16, 2026 11:54
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 10 out of 10 changed files in this pull request and generated 5 comments.


You can also share your feedback on Copilot code review. Take the survey.

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

@robson90 thanks for the PR! I have a question, I might've missed it earlier, but what's exactly the decision behind putting the interfaces into the REST module instead of API module (in the same package)?

Although this seems to bloat only the test classpath, I think it make sense to pull as little transitive dependencies as possible to keep it clean. According to my quick check it is 197 vs. 66 transitive deps.

@robson90
Copy link
Copy Markdown
Contributor Author

Dear @Pepo48 ,

regarding:

@robson90 thanks for the PR! I have a question, I might've missed it earlier, but what's exactly the decision behind putting the interfaces into the REST module instead of API module (in the same package)?

Although this seems to bloat only the test classpath, I think it make sense to pull as little transitive dependencies as possible to keep it clean. According to my quick check it is 197 vs. 66 transitive deps.

As far as I understood @vmuzikar wanted everything related to Admin Api V2 inside one module. Why the module structure has been chosen like this, idk.

Regarding the dependencies, maybe I can add some exclusions. So we are only importing the relevant Interfaces and nothing more. I assume you used mvn dependency:tree, right ?

Thanks for the suggestions, will fix them !

@vmuzikar
Copy link
Copy Markdown
Contributor

what's exactly the decision behind putting the interfaces into the REST module instead of API module (in the same package)?

I don't remember. :D But we could move it to the api module of course.

@Pepo48
Pepo48 commented Mar 17, 2026
Copy link
Copy Markdown
Contributor

@robson90 yes, I used mvn dependency:tree. Agree to exclude the deps, it might be actually even better idea as it most likely pulls even less dependencies. Thanks, good job!

@robson90
Copy link
Copy Markdown
Contributor Author

@Pepo48 good catch, I used this to count the dependencies:

mvn dependency:tree | grep -E "^\\[INFO\\] [+|\\-]" | wc -l
Before it were 210, now with the exclusion its only 58.

vmuzikar
vmuzikar previously approved these changes Mar 18, 2026
@vmuzikar vmuzikar 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.

@robson90 LGTM, thank you.

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;

public class StreamDeserializer extends JsonDeserializer<Stream<?>> implements ContextualDeserializer {
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.

@shawkins Do you want to review too, especially the deserializer?

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.

I'm trying to understand this better. On the server side we are registering a StreamSerializer (

module.addSerializer(new StreamSerializer(streamType, type));
) - it seems quite odd that Jackson does not provide, yet requires a Stream deserializer.

From the discussion in #46973 (comment) it would be good to understand exactly what quarkus is doing with the Stream (is it creating a full response buffer for error handling purposes?).

I know in some places we have resorted to using Response, and in particular the response stream directly for performance reasons

response.entity((StreamingOutput) consumer::accept);
- these endpoints may also need something more idiomatic on the interface.

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 assume Quarkus is handling Stream<> as like the return type would be List<ClientRepresentation>. Because when I debugged the failing test, I can see this Debug Log:

2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> GET /admin/api/default/clients/v2 HTTP/1.1
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> Accept: application/json
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDYnIxV1lTN2U4cGRJV00wZ1Nrck1qVTVCNi1ITndsb0ZuYk5mQ0otdS13In0.eyJleHAiOjE3NzM4Mzk1ODAsImlhdCI6MTc3MzgzOTUyMCwianRpIjoidHJsdGNjOmU5ZThjOWM2LWM2MWQtZjA5NS0xZmIyLTdlOGU0ZWJmNDk0YSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbWFzdGVyIiwic3ViIjoiZjc2YTQ5MjUtNTQxYi00YTk4LWFkODgtNTkxNDQ3YzQwZTg0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVtcC1hZG1pbiIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSJ9.O-3JuVtzbXOuudmwLNVOJkFpr4SE2P8dRz-AggfKM8zFE98XGyoCO3n1G3h0_WJjwFF9GSMBbuMPBEMNsumfhxKI-tgPUA08stuvCczoV2ZqPFAkbvUcWNX0u9s_RxYhVMRliKdugaSwAH2uD145dGC_YnGy7S8wmAURUo0tFShy7UIslO5_kytXw0G6Cf9fCg327yr0-zzI7S-jdV2qmHHYdNlJXCxRWASs6slVrpAbLlZf55YjmVKkGDyXebmeC6T08kCZ19WnqZjo3knHi6SLVFnAow0hGLVLniFxbGTCn7t7g-HTw_BVsCQdvbjtI4joesj17d6YFgoZrYF1YQ
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> Host: localhost:8080
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> Connection: Keep-Alive
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 >> User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.18)
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "GET /admin/api/default/clients/v2 HTTP/1.1[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "Accept: application/json[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDYnIxV1lTN2U4cGRJV00wZ1Nrck1qVTVCNi1ITndsb0ZuYk5mQ0otdS13In0.eyJleHAiOjE3NzM4Mzk1ODAsImlhdCI6MTc3MzgzOTUyMCwianRpIjoidHJsdGNjOmU5ZThjOWM2LWM2MWQtZjA5NS0xZmIyLTdlOGU0ZWJmNDk0YSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbWFzdGVyIiwic3ViIjoiZjc2YTQ5MjUtNTQxYi00YTk4LWFkODgtNTkxNDQ3YzQwZTg0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVtcC1hZG1pbiIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSJ9.O-3JuVtzbXOuudmwLNVOJkFpr4SE2P8dRz-AggfKM8zFE98XGyoCO3n1G3h0_WJjwFF9GSMBbuMPBEMNsumfhxKI-tgPUA08stuvCczoV2ZqPFAkbvUcWNX0u9s_RxYhVMRliKdugaSwAH2uD145dGC_YnGy7S8wmAURUo0tFShy7UIslO5_kytXw0G6Cf9fCg327yr0-zzI7S-jdV2qmHHYdNlJXCxRWASs6slVrpAbLlZf55YjmVKkGDyXebmeC6T08kCZ19WnqZjo3knHi6SLVFnAow0hGLVLniFxbGTCn7t7g-HTw_BVsCQdvbjtI4joesj17d6YFgoZrYF1YQ[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "Host: localhost:8080[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "Connection: Keep-Alive[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.18)[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 >> "[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "HTTP/1.1 200 OK[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "content-length: 4179[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "Content-Type: application/json;charset=UTF-8[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "Referrer-Policy: no-referrer[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "Strict-Transport-Security: max-age=31536000; includeSubDomains[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "X-Content-Type-Options: nosniff[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "X-Frame-Options: SAMEORIGIN[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "X-Robots-Tag: none[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "[\r][\n]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.wire] (main) http-outgoing-1 << "[{"clientId":"account","displayName":"${client_account}","enabled":true,"appUrl":"/realms/default/account/","redirectUris":["/realms/default/account/*"],"roles":["manage-account","view-applications","view-consent","view-groups","manage-account-links","manage-consent","delete-account","view-profile"],"loginFlows":["STANDARD"],"protocol":"openid-connect"},{"clientId":"account-console","displayName":"${client_account-console}","enabled":true,"appUrl":"/realms/default/account/","redirectUris":["/realms/default/account/*"],"loginFlows":["STANDARD"],"protocol":"openid-connect"},{"clientId":"admin-cli","displayName":"${client_admin-cli}","enabled":true,"loginFlows":["DIRECT_GRANT"],"protocol":"openid-connect"},{"clientId":"broker","displayName":"${client_broker}","enabled":true,"roles":["read-token"],"loginFlows":["STANDARD"],"auth":{"method":"client-secret"},"protocol":"openid-connect"},{"clientId":"mixed-test-oidc","description":"OIDC client for mixed protocol test","enabled":true,"loginFlows":["STANDARD","DIRECT_GRANT"],"webOrigins":["http://localhost:3000","http://localhost:4000"],"protocol":"openid-connect"},{"clientId":"mixed-test-saml","description":"SAML client for mixed protocol test","enabled":true,"nameIdFormat":"email","forceNameIdFormat":false,"includeAuthnStatement":true,"signDocuments":true,"signAssertions":true,"clientSignatureRequired":true,"forcePostBinding":true,"frontChannelLogout":false,"signatureAlgorithm":"RSA_SHA256","signatureCanonicalizationMethod":"http://www.w3.org/2001/10/xml-exc-c14n#","signingCertificate":"MIIErTCCApUCBgGdARMXLDANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9taXhlZC10ZXN0LXNhbWwwHhcNMjYwMzE4MTMxMDIxWhcNMjkwMzE4MTMxMjAwWjAaMRgwFgYDVQQDDA9taXhlZC10ZXN0LXNhbWwwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiv3/qyQcSTirZ7k4f9gmRwHvcmGU3Cba7EI/no3EVagcBDaeFpZPRax5eUTDgPRQQ7c46L0FEljQ+VAZ8eZpdIhzBTS0jny9c+1ZKCum4H1gGh3ePrLo5iMiNZ9xJRq+iAsU7BhycuR5uteOqH6bRxSlO9rkgcyfUA+2Cl/rQLiOaqSU/HxBVXI62pe+E/2mixQlS1iKH8Jt2KmHK3ELprH2678mU5LlHpznx5iMJX5Ih/rsGLGXTDA3K2MPgPsHn+ayyTFkujcltDG/p71yyCBr5ymhAx/xQ4bSQbFDLIcba8yR7vJpVqxVHk/BjEUrNtMOMm1MQA3AxupqAaP/aRyATSgUlROzEukhz/ghWSRGMo3cEjq3oYXMQgrWbnCVIeWacVzbJzxYpQR5ABlhY9If3VymXgQKGAjA3chDd8rHHwr+oPTzAEk8ZjZkWfOogX5gYFUchK9XDdelv338c94AcSz5/eUImkzNNIyCtNa+PhGqHfBcXOlOLB3J7mH6Hzn/vNXxoyrxsT1hR/Fm4iL8ZVQ8btdXv3jZ14m8uEUoMZc0zJfq+jGDxzV1QF/VJMFFgYSCduawGyycUKG+pGfRQeZxpqFmtV0t+jBgJecTdj/TOXbhXG9+7ptngDIzxEZgBBRqSoptOKYL6mwy1AXa1NCO4TDlinEM+jvhwkQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCGpbWga8PutsKZu5vXWp8iDFQDHnYxvdJuBiiqA8dPuCkqC8sxwqUkQn6SruyLRnry17N+BLYzlECxK3aBeLmCa7fiWdhS92NSDdyowqA5X69XMTStmzK9MuIdqmfhHO+AsrOZVNIizUI/sPRAb+SwITJBLNnIyyKH+LNDNWsB2QG+vLYVje/qDnuXBWkn2bsckf7x9kXq+LydPKzI0Aj4mi+4ge4R4PPTI9VTgxaPd+IASh/XtbvFhWLrEMcS74pppST+QAPMKc6sWAMf6m3OhCbq7DQPPaEg3yqUMNoWlnD4CXkZc4503QHUjy1/wkg1x6p6dzs3RCZRt4nN8tM19PH5K0iiArlKWvjVOB9JAnIfkKhkbCieNmeImSt4bRhKlqkH8KsAfa8HRfEkzh+gHINZEk8Uk0EmNctsz2IbFultKIBLkXoFspb3ILy8WHmggwtlRl19bzQYuimIamOLX/nrnEgiCWd7A9IzqgX/C5fYQaWWVSHPE4EE1wCqxCWz0wPpj98KjTwiaP+GySj3cfb2xlnOe4RUK9vLILWPe0ofWmlVB1oJ/hQjtpOhy6+EO4t6vAKIGKCUz05XNnjV7sPfbiKeTaeOO7USMNRLUTjgR8+UpdEBF5vxV9pQPlKNyYlaE7nPFR8U65s2W4wQH20cdjE5NRdE+MtukZQgdQ==","allowEcpFlow":false,"protocol":"saml"},{"clientId":"myclient","enabled":true,"loginFlows":["SERVICE_ACCOUNT","STANDARD"],"auth":{"method":"client-secret","secret":"mysecret"},"serviceAccountRoles":["default-roles-default"],"protocol":"openid-connect"},{"clientId":"realm-management","displayName":"${client_realm-management}","enabled":true,"roles":["view-realm","view-identity-providers","manage-identity-providers","impersonation","realm-admin","create-client","manage-users","query-realms","view-authorization","query-clients","query-users","manage-events","manage-realm","view-events","view-users","view-clients","manage-authorization","manage-clients","query-groups"],"loginFlows":["STANDARD"],"auth":{"method":"client-secret"},"protocol":"openid-connect"},{"clientId":"security-admin-console","displayName":"${client_security-admin-console}","enabled":true,"appUrl":"/admin/default/console/","redirectUris":["/admin/default/console/*"],"loginFlows":["STANDARD"],"webOrigins":["+"],"protocol":"openid-connect"}]"
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << HTTP/1.1 200 OK
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << content-length: 4179
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << Content-Type: application/json;charset=UTF-8
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << Referrer-Policy: no-referrer
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << Strict-Transport-Security: max-age=31536000; includeSubDomains
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << X-Content-Type-Options: nosniff
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << X-Frame-Options: SAMEORIGIN
2026-03-18 14:12:01,626 DEBUG [org.apache.http.headers] (main) http-outgoing-1 << X-Robots-Tag: none

as the payload is looking like a list.

I think there are two different things that are important:

  1. What is the return type from Keycloak.
  2. How does the adminClient interact with Keycloak. As in the discussion above, the adminClient is calling the endpoint with a synchronize invoke method. Which means every performance benefit is gone, as it is waiting for the complete response.

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.

I assume Quarkus is handling Stream<> as like the return type would be List

Yes, can confirm the StreamSerializer just writes an array.

From a server side perspective I confirmed that is happening in a streaming manner as the serialization is directly into the output stream.

What is the return type from Keycloak.

When we are returning a "list" the interface will be one of List (array), Stream, or Multi.

Unless I'm missing something, the response body would be identical no matter the type. The client proxy logic only knows about List by default, but we don't want to use that when the server logic is actually streaming.

Based upon what you have seen here, the use of Stream or Multi will require a custom deserializer.

In more exotic scenarios we'll return a Response. I haven't checked how a RestEasy proxy handles a raw Response, but that would at least require additional client side logic to consume.

Switching to an InputStream would not good either as that negates the server side simplicity of directly returning the objects.

How does the adminClient interact with Keycloak. As in the discussion above, the adminClient is calling the endpoint with a synchronize invoke method. Which means every performance benefit is gone, as it is waiting for the complete response.

We'd need async parsing support as well.

The only simple solutions here seem to be:

  1. keep adding deserializers as needed, and double check how Response is handled via a client side proxy.
  2. introduce or generate client side specific interfaces such that Stream is replaced by List

Another thought is that successful response codes are basically not available via the proxy (or can they be obtained somehow?). Our PUT can return differentiated codes (created vs updated), so you won't know which occurred.

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.

2. introduce or generate client side specific interfaces such that Stream is replaced by List

That's something we did with v1. If possible, I'd like to avoid doing that again (especially if not generated but maintained manually).

I'm quite surprised resteasy doesn't support a basic stream deserializer OOTB. So we'll need to add custom one but it's not ideal of course.

In more exotic scenarios we'll return a Response. I haven't checked how a RestEasy proxy handles a raw Response, but that would at least require additional client side logic to consume.

Yes, it seems so:
https://github.com/robson90/keycloak/blob/b301d02b1ea8c1769e820ec1b2f2b14a8aae2a43/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java#L368

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.

Yes, it seems so:
https://github.com/robson90/keycloak/blob/b301d02b1ea8c1769e820ec1b2f2b14a8aae2a43/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java#L368

Ah, so that the concern about alternative success codes as well - any time we have to return one we'll be using a response. So then we'll just need some helper logic for consuming Responses.

Something we need to do in a follow-up is add APIResponses to everything that is returning a Response:

    @Operation(summary = "Create or update a client", description = "Creates or updates a client in the realm")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BaseClientRepresentation.class))),
            @APIResponse(description = "created", responseCode = "201", content = @Content(schema = @Schema(implementation = BaseClientRepresentation.class))) })

The client side handling of a raw Response is still not ideal.

There is a resteasy reactive RestResponse construct that is typed - are we allowed to use that or do we need to stick to plain jax-rs? I also haven't check yet how that is handled on the client side.

Alternatively if we can't use that, we could consider introducing a TypedResponse class that extends Response (which may not be allowable) and delegates to an actual response, then wrap the target proxy with another proxy that detects when a method is called that returns a TypedResponse and does the conversion there such that getEntity returns the expected type.

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.

Something we need to do in a follow-up is add APIResponses to everything that is returning a Response

Captured as #47365.

The client side handling of a raw Response is still not ideal.

+1

There is a resteasy reactive RestResponse construct that is typed - are we allowed to use that or do we need to stick to plain jax-rs?

AFAIK we need to be as platform/lib agnostic as possible in our endpoints.

Alternatively if we can't use that, we could consider introducing a TypedResponse class

That would sound good. But we could do that as a follow-up.

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.

we could consider introducing a TypedResponse class

Captured as #47367.

Pepo48
Pepo48 previously approved these changes Mar 18, 2026
Co-authored-by
10BC8
: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>

Update rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java

Co-authored-by: Peter Zaoral <pepo48@gmail.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
// The client must work with the newer versions of Keycloak server, which might contain the JSON fields not yet known by the client. So unknown fields will be ignored.
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

objectMapper.registerModule(STREAM_MODULE);
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.

As discussed on the meeting, the concerns about including it into the admin client - we could conditionally register it if the Client V2 feature is enabled. It'll help to decrease the probability that something is broken for the admin client core itself.

cc: @robson90 @keycloak/cloud-native WDYT?

@robson90 robson90 Mar 24, 2026 •
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.

IMHO the safest path here would be to make it return List<> for now.

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.

Based upon what the module is doing, it is safe to leave this as is.

Swiching to List is not desirable as that can have a heavier memory footprint server side.

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.

It's an admin client, it doesn't know what features are enabled. We could, hoverer, hide it behind some system property toggle?

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.

If @shawkins has reviewed the logic and has the context, I'm ok to leave it as is.

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.

Swiching to List is not desirable as that can have a heavier memory footprint server side.

I understand that, but you are moving the memory footprint from the Server to the Client, as the Client is creating a list from it and then streams it.

Somewhere above are the Debug logs from the Payload, that Keycloak is returning. For me that looks like a normal List<> as response. Im unsure, if we are getting any real benefit here.

Im fine with Stream<> for now.

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

@robson90 Nice work! Just a few minor comments. It'd be nice to find out if there's a better alternative for the custom stream deserializer, though.

Comment thread rest/admin-v2/tests/pom.xml Resolved
@vmuzikar
vmuzikar requested a review from mabartos March 25, 2026 07:54
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
@vmuzikar vmuzikar 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.

Still LGTM, thanks.

public String getClientsApiUrl() {
return "http://localhost:8080/admin/api/%s/clients/v2".formatted(getRealmName());
}

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.

Just a nitpick. Not necessary change.

@vmuzikar
Copy link
Copy Markdown
Contributor

@mabartos Do you want to give it another look?

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

It'd be nice to have the wrapper on the abstract class level, but LGTM - it can be done as a follow-up and definitely not blocking.

If others are ok with the StreamDeserializer, I'm ok with that too.

@vmuzikar
vmuzikar merged commit d7091c7 into keycloak:main Mar 27, 2026
90 checks passed
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.

Client API v2 Java client

7 participants

0