Repository navigation
Release 1.11.0: OpenAPI Plugin rework: a richer spec, honest schemas and fail-fast diagnostics #202
MoonWorm
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Overview
1.11.0 is a rework of the OpenAPI plugin, plus the configuration and bootstrap that feed it. Four headline changes:
filter[…]parameters, sortable fields, limit-offset pagination, deprecation, and a resource identifier described once and reaching every operation that takes one. Resourcetypeis now an enum, which makesincludedresolvable: a generated client deserializes each member into its concrete class instead of handing you a union to switch on.requiredis a create-only promise. A response may legitimately omit any attribute — an unset value is never serialized, sparse fieldsets select a subset, access control withholds what the caller may not see — so attributes are published in three forms:<Type>CreateAttributeskeepsrequired,<Type>UpdateAttributesdrops it (PATCHis partial), and<Type>Attributesrequires nothing.@AccessControlrequirements become OpenAPI security requirements, with each scope published only on the grant flow that declares it — a scope your client-credentials flow never declares is a reference to nothing.403appears on writes a requirement actually guards, and anAuthenticated.ANONYMOUSoperation is published as public.$refresolving to nothing breaks client generation and response validation — andjsonapi4j.oas.diagnosticsdecides what that costs you, up to refusing to start.Beyond the plugin: Spring Boot 3 and 4 from one artifact, a Quarkus compatibility matrix, supported version floors, and configuration validated at startup rather than on first use.
This release contains breaking changes. They are listed first.
Breaking Changes
jsonapi4j.oas.customResponseHeadersis goneThe property and its
ResponseHeader/CustomResponseHeaderGrouptypes are removed. Response headers — and anything else the framework cannot derive — are now expressed as anOasCustomizer:Register it as a
@Bean(Spring Boot) or@Produces(Quarkus); with the plain Servlet API pass it tonew JsonApiOasPlugin(oasProperties, List.of(new RateLimitHeadersCustomizer())). Customizers run after the built-in ones, so each sees the finished document.requiredno longer appears on response attribute schemasIf you generate clients from the document, regenerate them: attributes previously modelled as non-nullable are now nullable, and the write bodies reference
<Type>CreateAttributes/<Type>UpdateAttributesrather than<Type>Attributes.This is a correction, not a relaxation. A
requiredlist copied onto responses rejects three things the server is entitled to send: an attribute with no value (never serialized), a sparse-fieldset selection, and an attribute access control withheld. A generated client modelled the field as non-nullable and then failed to read a valid reply.@Schema(requiredMode = REQUIRED)still means what it says — it is published on<Type>CreateAttributes, and only for resources that have a create.NONE_OFis gone from scope requirementsAccessControlScopes.ModeandScopesGroup.ModekeepANY_OFandALL_OF. "Deny if the caller holds this scope" cannot be stated soundly in OpenAPI and did not survive being published. Entitlements are unaffected and keepNONE_OF.Plugins are registered through
PluginRegistryList<JsonApi4jPlugin>is replaced byPluginRegistry, built withPluginRegistry.builder().register(…).build()and passed to the registries and toJsonApi4j.builder(). Spring Boot and Quarkus assemble it for you; plain Servlet bootstraps pass it explicitly.Configuration is validated at startup
Every enabled plugin's configuration is checked when
JsonApi4jis built, and a misconfigured one throwsPluginMisconfigurationExceptionwith every error at once rather than failing on the first bad key at first use. Serving the OpenAPI document from the same path asjsonapi4j.rootPath, aninfo.titlethat is blank, a non-x-extension key, a malformed URL — all now stop the boot.Supported version floors
Java 17, Spring Boot 3.0.0, Quarkus 3.20, Jakarta Servlet 6.0.0 — each the oldest version still supported. The framework compiles against the floor so that using anything newer is a compile error here rather than a runtime failure in your application. Quarkus 3.15 no longer augments.
The OpenAPI Document
Query parameters are published when you declare them
The framework parses
sort,filter[…]andpage[…]on every request, but only your operation can act on one — so the document advertises what you declare, not what the request layer tolerates. This is the most common surprise: a workingsortthat no client can discover.sortableFieldsbecomes the parameter's allowed values in both directions. A filter is declared by its dimension, not its parameter name: the framework spells itfilter[region], makes it optional and multi-valued as JSON:API defines filters, and bounds it withvalidation.maxElementsInFilterParam. Limits configured underjsonapi4j.validationare projected into the schemas throughout, so there is nothing to restate.The identifier is described once
@OasResourceInfo(resourceIdDescription = …, resourceIdExample = …)reaches the{id}path parameter of every operation acting on one instance and theidmember of the resource schema. Declaring it per operation is what let one endpoint carry an example and the next one not.includedis resolvableEvery resource schema pins its
typeto the single value JSON:API allows for it —UsersResource.typeisenum: ["users"], not an open string, which is the same rule the framework enforces at runtime with a409. The union of resource schemas a document may carry declares adiscriminatorontype, so a generated client deserializes each member into its concrete class.Deprecation
Deprecation only ever widens: a resource takes its operations with it, an operation may retire on its own, and nothing opts back in. Java's
@Deprecatedis not read — use the attribute.Paging is discoverable
Collection and standalone to-many responses name their pagination members instead of leaving
linksan untyped map andmetaan open object:self,first,prev,next,last, andpagination.nextCursor/pagination.totalItems. Followinglinks.nextand sendingpagination.nextCursorback aspage[cursor]are the same request. Every member is optional — a cursor-paged read knows there is a next page, not how many there are.Repetition folded away
Responses and parameters that every operation repeated verbatim are published once under
componentsand referenced from there. Nothing the document says changes — a$refand the object it points at are the same thing to a reader — but the difference between two operations stops being buried under hundreds of lines of identical error responses. On the sample app it halves the file.What Access Control Publishes
With the Access Control plugin on the classpath, the document states what it enforces rather than what was hoped for:
@AccessControl(scopes = …)security, one entry per alternative —(A or B) and (C or D)becomes four@AccessControl(authenticated = ANONYMOUS)security: [], OpenAPI's way of saying an operation needs no authentication403— reads never carry one, since a denied read answers with an empty document and200descriptionEach scope is published only on the grant flows whose own
scopesdeclare it. OpenAPI reads an operation's scopes against the scheme naming them, so a scope your client-credentials flow never declares is a reference to nothing — Swagger UI cannot offer it, linters reject it, and it claims a token that flow can never issue. A scope no flow declares fails generation: that is a typo, not a choice.Access control also describes what it withholds on the attribute itself, so absence is something a caller can act on rather than a mystery:
Each of these appears only when its plugin is registered and enabled — an application running neither access control nor sparse fieldsets is never told about them.
Diagnostics
jsonapi4j.oas.diagnosticsdecides what a document the plugin cannot vouch for costs you, in increasing order of strictness:DISABLEDWARN(default)FAIL_ON_REQUEST500with a JSON:API error documentFAIL_ON_STARTUPFAIL_ON_STARTUPis the only mode that builds the document eagerly, and that is the point of it: every other mode discovers a broken document on whichever request happens to ask for it first, which in a deployed application means discovering it in production. It costs the generation work at every boot, so an application that cannot afford that but must not publish a document it cannot vouch for wantsFAIL_ON_REQUEST.The setting governs reports only. A document that would say something untrue is refused whatever the mode — two schemas claiming one name would publish one of them under the other's shape, and nothing in the document would say so.
Recommended for CI:
jsonapi4j.oas.diagnostics=FAIL_ON_STARTUP. On Quarkus this is build-time fixed, like the rest ofjsonapi4j.oas.*.Spring Boot 4, Quarkus, and Version Floors
jsonapi4j-rest-springbootis one artifact supporting Spring Boot 3 and 4. It is compiled against the 3.0.x floor — so a Boot-4-only API is a compile error here rather than a runtime failure in a Boot 3 application — and the same sources are verified against Boot 4.1 and Spring Security 7 weekly and at release.Quarkus is covered by a compatibility matrix run weekly across the supported range, with the floor at 3.20. The Quarkus extension registers what a native image needs — the OpenAPI model, your attributes classes, the plugin's canned error examples — and the native build is exercised by an integration test.
jsonapi4j.oas.oasRootPathis now honoured identically by all three integrations; Spring Boot previously ignored it.Other Changes
GETandHEADonly, negotiates YAML throughAcceptas well as?format=, and serves anETagso a caller that already has the document gets a304.Migration Checklist
jsonapi4j.oas.customResponseHeaderswith anOasCustomizerbean.<Type>CreateAttributes/<Type>UpdateAttributes.NONE_OFscope requirements; entitlements keep the mode.PluginRegistryinstead of passingList<JsonApi4jPlugin>.sortableFieldsandfilterson operations that honour them, or they stay undocumented.@AccessControlrequires is declared under the grant flow that grants it.jsonapi4j.oas.diagnostics=FAIL_ON_STARTUP.Resources
All reactions