Conversation
Javalin 5.0.1 -> 7.2.2 and graphql-kotlin 5.5.0 -> 10.2.0, plus Kotlin 2.2,
Java 17 and Gradle 9. This is a breaking change for users of the module.
Javalin 7 replaced the `Plugin` + `PluginLifecycleInit` interfaces with
`Plugin<CONFIG>`, so the plugin now registers its routes from `onStart`, and
applications register it with `config.registerPlugin(...)`.
graphql-kotlin removed its own `GraphQLContext` marker interface in favour of
graphql-java's map-like `GraphQLContext`. Two consequences for users:
- `GraphQLPluginBuilder` no longer has a context type parameter, and a
context factory returns a `GraphQLContext` built from a map;
- a resolver no longer receives the context as a parameter. Take a
`DataFetchingEnvironment` and read `environment.graphQlContext` instead.
Subscriptions are now driven by kotlinx `Flow` rather than a reactive-streams
`Publisher`, so resolvers return `Flow` and the reactor dependency is gone.
`GraphQLRun` went with it: `GraphQLRequestHandler.executeSubscription` does
the same job, error handling included. `JavalinDataLoaderRegistryFactory` is
also gone, superseded by `KotlinDataLoaderRegistryFactory`.
The request parser no longer asks the mapper for the sealed
`GraphQLServerRequest`: graphql-kotlin annotates it for Jackson 3 and
fastjson2 while Javalin defaults to Jackson 2, so we pick the concrete
subtype and keep working with whatever `JsonMapper` the application uses.
Also fixes the publishing block, which was a copy-paste from javalin-openapi:
the artifact name was misspelled and the POM description and URL pointed at
that project.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The WebSocket endpoint used to speak an ad-hoc protocol: send a query frame, receive bare result data. It never matched any standard, so no off-the-shelf GraphQL client could talk to it. It now implements graphql-transport-ws: https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md connection_init -> connection_ack, ping -> pong, subscribe with an operation id, next, complete, and the protocol close codes (4400 bad request, 4401 unauthorized, 4409 duplicate id, 4429 too many init requests). The message types are graphql-kotlin's. Incoming frames are dispatched on their `type` field rather than deserialized into the sealed `GraphQLSubscriptionMessage`, which is annotated for Jackson 3 and fastjson2 while Javalin defaults to Jackson 2 — same reasoning as the HTTP parser. This also removes a defect that predates the migration: the handler used to run `runBlocking` around the collector inside `onMessage`, so a long-running subscription pinned a Jetty thread for as long as it lasted. Each subscription now runs in its own coroutine, keyed by operation id, and is cancelled by the client's `complete` message or when the connection closes. Known gap, in Javalin rather than here: `WsConfig` exposes no subprotocol negotiation, so the server cannot echo `Sec-WebSocket-Protocol: graphql-transport-ws`. RFC 6455 does not require it and the reference client does not check for it, but it is worth raising upstream. Tests go from 9 to 13 and now exercise the protocol itself, including acknowledgement, ping/pong, clean stream termination and two error paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the question raised in #5 and supersedes #6, which added a module-info.java at the repository root where nothing ever compiled it, and required `com.expediagroup.graphql.generator.annotations` — not a module name that exists. A real module-info.java is still not possible, and the blocker is upstream: graphql-kotlin-server, graphql-kotlin-schema-generator and graphql-kotlin-dataloader ship neither a module descriptor nor an Automatic-Module-Name, so they resolve as automatic modules named after their file names (graphql.kotlin.server and friends). Requiring those would freeze unstable, filename-derived names into our descriptor; the day ExpediaGroup declares proper names, every consumer breaks. Gradle already refuses to put such jars on the module path, so the descriptor does not even compile. Declaring Automatic-Module-Name is the part that can be done correctly today: a modular application can `requires io.javalin.community.graphql`, and the name is ours to keep stable when a full descriptor becomes possible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README documented APIs that never existed in this plugin — a `CommandGraphql` interface and a `@GraphQLContext` annotation — alongside a `GraphQLOptions.context()` call that is not there either, so it was wrong even for 5.x. It now describes the 7.x API and carries a migration section covering the four changes that break existing users: plugin registration, the context model, context no longer being injected into resolvers, and subscriptions moving from `Publisher` to `Flow` and onto the graphql-transport-ws protocol. Also documents the object mapper requirement, since Javalin 7 no longer bundles one, and the JPMS situation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous note claimed the server could not echo `Sec-WebSocket-Protocol: graphql-transport-ws`. That is wrong: verified against a running server, the Jetty handshake does echo it, so a graphql-transport-ws client connects. What it actually does is echo whichever subprotocol the client requested first, without validating it — asking for a made-up subprotocol gets a 101 and that same value back. So interop works, but the plugin cannot reject a client speaking a protocol it does not support, such as the legacy `graphql-ws` subprotocol. That is the part that would need a Javalin change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repository had no CI that built or tested anything. The only workflow deployed to Reposilite on a tag, while the README badge claimed "Test all JDKs on all OSes" and linked to it — the badge was reporting on a job that never ran a test. Adds a build workflow that compiles and tests on JDK 17 and 21 across Linux, macOS and Windows, plus a detekt job, and uploads reports when a job fails. The badge now points at that workflow and says what it actually measures. The deploy workflow keeps its job but moves to current action versions and is renamed to match what it does. Adds detekt with `buildUponDefaultConfig`. Twelve findings were raised: the real ones are fixed (a swallowed exception now logs, a backtick parameter name, a long line, wildcard imports), and the deliberate ones are suppressed at the call site with the reason written next to them rather than hidden in a baseline. Two rules are configured with a justification in detekt.yml. Adds four ADRs, using the same template as the author's other projects, for the decisions taken in 7.0: the context model, the subscription protocol, JSON mapping across the Jackson 2/3 split, and JPMS. These are the "why" that the code cannot carry on its own, and the module needs them more than most: it is looking for a new owner. No arc42 and no use case corpus: at ~500 lines of Kotlin the tests already name the behaviours, and the deployment views and quality scenarios arc42 asks for would have no content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deploy workflow only ran on a `v*` tag, so there was no way to publish a snapshot — and tagging one would say "release" about something that is not. Adds `workflow_dispatch`, and a guard: a manual run refuses to proceed unless the version ends in `-SNAPSHOT`, so releasing stays tied to a tag. The job now also builds and tests before publishing, which it never did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 9, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This brings the module up to date. It is a major version: the public API breaks, mostly because graphql-kotlin changed underneath it.
What changed
Plugin<CONFIG>API.GraphQLContext. graphql-kotlin deleted its marker interface, and it no longer injects context into resolvers, so a resolver takes aDataFetchingEnvironment.Flowinstead of a reactive-streamsPublisher, and are served over the graphql-transport-ws protocol. The 5.x exchange was invented for this plugin and matched no standard, so no off-the-shelf client could use it.GraphQLRunandJavalinDataLoaderRegistryFactoryare gone, superseded by upstream equivalents.Fixes along the way
runBlockingaround the collector insideonMessage, pinning a Jetty thread for the lifetime of a subscription. Each subscription is now a cancellable coroutine, keyed by operation id.CommandGraphqlinterface and a@GraphQLContextannotation.Added
docs/adrfor the four decisions that shape this release.Automatic-Module-Name, which is as far as JPMS can go today — see JPMS #5.Tests go from 9 to 13 and now exercise the subscription protocol itself, including acknowledgement, ping/pong, clean stream termination and two error paths.
Not included
No release. The version is
7.0.0-SNAPSHOTand nothing has been published anywhere. See #7 for the ownership question, which is the one thing this PR cannot answer.