# v0.4.9 (31 July 2026) {#v049-31-july-2026}

[v0.4.9 on GitHub](https://github.com/higher-kinded-j/higher-kinded-j/releases/tag/v0.4.9)

This release completes the `@GenerateMapping` **mapper programme**: every wire shape a REST boundary throws at a record domain now maps through one spec convention, under one null doctrine, rendering as one 422 response.

**The full mapper** ([#628](https://github.com/higher-kinded-j/higher-kinded-j/issues/628), [#645](https://github.com/higher-kinded-j/higher-kinded-j/issues/645), [#625](https://github.com/higher-kinded-j/higher-kinded-j/issues/625), [#624](https://github.com/higher-kinded-j/higher-kinded-j/issues/624), [#623](https://github.com/higher-kinded-j/higher-kinded-j/issues/623))

- **Bean-shaped wires**: a mutable getter/setter class or a builder-constructed immutable one (JAXB getter-only collections included) maps like a record, with the full feature set (renames, leaves, derived fields, container lifting, nesting both ways) and a domain `Optional<T>` bridging to a nullable property. `asIso()` stays truthful: an all-primitive bean earns it, a reference-reading one does not. See [Bean-shaped wire targets](../mapping/beans.md#bean-shaped-wire-targets).
- **Sparse PATCH write-back**: a spec extending `UpdateSpec<D, W>` (bean wire only) generates `updateFrom(Wire) : Edits.Accumulated<Domain>`, the REST PATCH contract as a validated fold: null means keep, present values are set or parsed, every invalid present field reports at once, and shapes that cannot honour null-as-absent (record wires, primitives, `Optional` bridges, sealed hierarchies) are rejected. The example app gains a worked `PATCH /api/users/{id}` endpoint ([#647](https://github.com/higher-kinded-j/higher-kinded-j/pull/647)). See [Sparse PATCH write-back](../mapping/beans_patch.md#sparse-patch-write-back-updatespec).
- **The validated `patch` tier**: a projection carrying fallible correspondences swaps `asLens()` for a dense `patch(domain, wire) : Validated<NonEmptyList<FieldError>, Domain>` write-back, validating every projected component at once while unprojected ones survive by construction. Deliberately the opposite of the sparse tier: `patch` treats a missing value as an error, `updateFrom` as keep-the-current-one. See [Leaf-carrying projections](../mapping/tiers.md#leaf-carrying-projections-the-validated-patch).
- **Generic records, three ways, all nestable**: concrete instantiations (`MappingSpec<Page<User>, PageDto<UserDto>>`), threaded specs (one generic Impl behind `instance()`), and element-mapped specs (an abstract `ValidatedPrism<TDto, T>` leaf supplied through a generated `of(...)` factory), with use sites resolving by type-argument unification and composing `of(...)` in place. Record-to-record only; raw and wildcard uses are diagnosed. See [Generic records](../mapping/generics.md).
- **Shared vocabulary**: specs may extend plain mix-in interfaces carrying renames, leaves and derived fields, collected with Java's own precedence and diagnosed with the declaring interface named. See [Shared vocabulary](../mapping/codecs.md#shared-vocabulary-mix-in-interfaces).
- Every tier is law-checked through `MappingLaws` (including located-validation clauses on both write-backs) and pinned by per-tier golden Impls, so generator drift fails the build.
- **Build tooling**: every annotation processor registers with Gradle's incremental annotation processing (nineteen in `hkj-processor`, plus the Spring client processor; [#677](https://github.com/higher-kinded-j/higher-kinded-j/issues/677)), so consuming source sets keep incremental compilation. Lombok interop is covered by test: a `@Data` class works as a bean-shaped wire, provided Lombok is listed **before** `hkj-processor` on the processor path (the reverse order is diagnosed). See [Manual setup](../tooling/manual_setup.md).
- **Injection and testing documented** ([#678](https://github.com/higher-kinded-j/higher-kinded-j/issues/678)): register the surface you consume as a bean (`ValidatedPrism` for parse-capable mappings; method references for `build`, `patch` and `updateFrom`), and fake it as a value with `ValidatedPrism.of(...)` (the interface is sealed, so mocking is impossible by design). The Spring example app demonstrates the seam end to end. See [Injecting and testing generated mappings](../mapping/testing.md#injecting-and-testing-generated-mappings).

**One null doctrine** ([#653](https://github.com/higher-kinded-j/higher-kinded-j/issues/653), [#659](https://github.com/higher-kinded-j/higher-kinded-j/issues/659), [#660](https://github.com/higher-kinded-j/higher-kinded-j/issues/660))

A JSON binder leaves a missing property `null` on a record component just as on an unset bean property, so every reference-typed read on every accumulating surface (`parse` on both wire shapes, `patch`, fallible `@GenerateMerge` legs, `ValidatedPrism.parseAll`/`parseValues`) is guarded into a located, accumulating `FieldError` (`must not be null`), never an exception; failures locate through nesting and inside containers by index or key (`customer.name`, `emails.1`), identity-copied containers included. The caller-contract boundaries: a null wire, source argument or map key stays `requireNonNull`, and the total directions (`build`, `asLens().set`, `asIso().reverseGet`) stay unguarded by declaration. A lossless record mapping keeps `asIso()`, its guards covering hostile bindings only.

_Behaviour changes against v0.4.8, all in the friendly direction but observable:_

- `parse` and fallible `assemble` on null-carrying wires previously threw `NullPointerException`; they now return located `Invalid` (a 422 instead of a 500 at a Spring boundary). This includes null elements inside containers, which previously threw from the bulk forms or passed through identity copies unexamined.
- List-element failures gain their index segment: `emails: not an email address` becomes `emails.1: not an email address` in 422 payloads (bracketed rendering stays deferred to the future path-segment model).
- The generator is stricter about silent mistakes: a locally declared leaf naming no domain component is now a compile error with a nearest-name hint (`Did you mean 'email()'?`) instead of silently validating nothing; spec methods colliding with generated members are rejected ([#654](https://github.com/higher-kinded-j/higher-kinded-j/issues/654)); and a leaf on a projected component now takes effect (selecting the `patch` tier) instead of being silently ignored.

**Spring: the 422 leg** ([#627](https://github.com/higher-kinded-j/higher-kinded-j/issues/627))

A controller returning any all-located-`FieldError` `Invalid` (a mapper `parse`, `patch` or `updateFrom` result, a `Path.fields()` assembly, an `@GenerateAssembly` result) renders as a single **422 Unprocessable Content** response listing every bad field, each with its display `path`, lossless `segments` and `message`. The leg is chosen by payload shape alone; the status is configurable via `hkj.web.validation-field-error-status`. Paths use domain component names, renames included. See [The 422 leg](../spring/spring_boot_integration.md#the-422-leg).

_Behaviour change:_ a pre-existing endpoint returning an all-`FieldError` payload moves from 400 to **422** on upgrade; set `hkj.web.validation-field-error-status: 400` to restore it. Tests and examples adopt Spring Framework 7's `HttpStatus.UNPROCESSABLE_CONTENT` (the RFC 9110 rename).

**hkj-spring hardening** ([#642](https://github.com/higher-kinded-j/higher-kinded-j/issues/642))

A review pass fixed a security fail-open in the JWT converter, async-handler lifecycle defects, a Jackson contextual-binding bug, and dead configuration.

_Breaking changes / migration:_

- **SSE responses commit after the first stream element resolves**, so a stream failing at its start returns the configured `hkj.web.vstream-failure-status` instead of a broken `200`; `EventSource.onopen` fires on the first element, so long-idle streams should emit an early heartbeat.
- **Removed dead `@ConfigurationProperties` surface** (source-breaking for programmatic-config consumers): `HkjProperties.Jackson.SerializationFormat` and the `*-format` accessors, `HkjProperties.Validation` (`hkj.validation.*`), the `hkj.async.executor-*` fields, `EffectConfig.startupValidation`/`interpreterSelection`, and `EitherAuthorizationManager.AuthorizationSuccess`. Delete the keys; none altered behaviour.
- **`hkj.security.validated-user-details` defaults to `false`** and starts empty (sample accounts moved to `ValidatedUserDetailsService.withSampleUsers()`); enabling it without registering accounts fails every login.
- **A JWT with a missing or malformed authorities claim is rejected (401)** instead of authenticating with empty authorities; set `hkj.security.reject-missing-authorities-claim: false` for lenient missing-claim handling.
- **Effect-boundary interpreter resolution fails fast at startup** on ambiguous or profile-gated-away matches, previously silent scan-order selections.

---

**Previous:** [v0.4.10](v0_4_10.md)
**Next:** [v0.4.8](v0_4_8.md)
