Skip to content

feat!: Javalin 7 + graphql-kotlin 10, with graphql-transport-ws subscriptions - #8

Merged
7agustibm merged 7 commits into
mainfrom
javalin-7
Aug 14, 2026
Merged

7agustibm merged 7 commits into
mainfrom
javalin-7

Conversation

@7agustibm

Copy link
Copy Markdown
Collaborator

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

  • Javalin 5.0.1 to 7.2.2, graphql-kotlin 5.5.0 to 10.2.0, Kotlin 2.2, Java 17, Gradle 9.
  • The plugin registers routes through the new Plugin<CONFIG> API.
  • Context follows graphql-java's map-like GraphQLContext. graphql-kotlin deleted its marker interface, and it no longer injects context into resolvers, so a resolver takes a DataFetchingEnvironment.
  • Subscriptions are driven by kotlinx Flow instead of a reactive-streams Publisher, 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.
  • GraphQLRun and JavalinDataLoaderRegistryFactory are gone, superseded by upstream equivalents.

Fixes along the way

  • The WebSocket handler used to runBlocking around the collector inside onMessage, pinning a Jetty thread for the lifetime of a subscription. Each subscription is now a cancellable coroutine, keyed by operation id.
  • The publishing block was a copy-paste from javalin-openapi: misspelled artifact name, and a POM description and URL pointing at that project.
  • The README documented APIs this plugin never had — a CommandGraphql interface and a @GraphQLContext annotation.

Added

  • A build workflow: JDK 17 and 21 across Linux, macOS and Windows, plus detekt. There was no CI that built or tested anything, while the badge claimed "Test all JDKs on all OSes" and linked to a deploy-only job.
  • detekt, with deliberate exceptions suppressed at the call site with reasons rather than hidden in a baseline.
  • A migration guide in the README, and ADRs in docs/adr for 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-SNAPSHOT and nothing has been published anywhere. See #7 for the ownership question, which is the one thing this PR cannot answer.

7agustibm and others added 7 commits August 9, 2026 12:42
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
@7agustibm
7agustibm merged commit 7290bd6 into main Aug 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant