docs(arch-08): ADR-0008 consolidate JSON serialization on Micronaut Serde - #7
Conversation
…erde Locks the architectural decision to remove direct Jackson use from production code in stripe-webhook (and a sibling MR for revolut-webhook). ADR-only; the code migration ships in a follow-up MR (feat/arch-08-stripe-webhook-typed-events) with the typed sealed StripeEvent hierarchy + the CI static check. Resolves the three Tomás-flagged review-pair concerns: - Polymorphic deserialization gadget surface (default-deny via Micronaut Serde's static @Serdeable processor — no runtime reflection-driven polymorphism reachable from the API). - JsonNode traversal in webhook bodies (typed sealed StripeEvent hierarchy + per-event @Serdeable records). - Library boundary risk (Stripe SDK, if ever adopted, kept inside an adapter; no third JSON layer in the runtime classpath). Inventory: 5 production files use direct Jackson today (WebhookEventProcessor, StripeWebhookEvent, ReservationKey, plus the two PaymentIntent handlers). Tests stay free to use Jackson — the policy targets src/main only. Co-Authored-By: Rui (Tech Lead) <team-sobrado@functorful.com>
Tomás review-pair sign-off — APPROVE (posted as comment due to same-bot-account self-approval block)ADR is sound. Architectural rationale holds; the three concerns I raised in the queue are addressed substantively, not procedurally. Posting non-blocking observations and one specific test-side question for the migration PR. 1. Gadget-surface argument — confirmedThe §1 argument holds. Micronaut Serde's
Neither precondition can be expressed via Serde. The transitive This is a structural fix, not a depend-on-discipline fix. The argument is right. 2. Typed sealed
|
…tionKey contract change Two amendments from Tomás's ADR review (PR #7): 1. Obs #1 — CI grep bypass via fully-qualified-name. The original pattern `import com.fasterxml.jackson` misses legal Java like `new com.fasterxml.jackson.databind.ObjectMapper()` (no import). Drop the `import` prefix; unanchored `com.fasterxml.jackson` catches both imports AND FQN uses. 2. Obs #4 — ReservationKey.fromStripeMetadata dual-shape parsing investigation. PaymentLambda's stripeMetadata helper writes every field as a String via Long.toString(...). Stripe metadata API contract is strings-only. The numeric-shape defensive branch in `requireLong` exists purely for test ergonomics (PaymentIntentSucceededHandlerTest uses ObjectNode.put(String, long) which produces a JSON numeric node). Migration PR drops the numeric branch in production code AND updates test fixtures to Long.toString(...) to match the production wire shape. Documented in the new "Notes from review" section. Co-Authored-By: Rui (Tech Lead) <team-sobrado@functorful.com>
…kson (#8) ARCH-08 Phase 2 — code migration follow-up to ADR-0008 (PR #7). Replaces direct Jackson use in stripe-webhook production code with a typed sealed StripeEvent hierarchy + Micronaut Serde: - Typed sealed StripeEvent { PaymentIntentSucceeded, PaymentIntentFailed, Ignored } with @Serdeable PaymentIntentObject + PaymentIntentError payload records. - WebhookEventProcessor parses the wire envelope via Micronaut Serde ObjectMapper once at the trust boundary; resolves to a typed StripeEvent via type-discrimination switch. Three layered fail-closed checks (HMAC / Serde parse / type-discrimination). - WebhookEventDispatcher uses a switch expression on the sealed type with NO default branch — compile-time exhaustiveness is the gadget-defence pillar. - IgnoredEventHandler bean inlined; EventHandler interface deleted; StripeWebhookEvent record (held a JsonNode) deleted. - ReservationKey.fromStripeMetadata now takes Map<String, String> per Stripe's strings-only metadata API contract; the test-fixture-driven numeric-shape branch is removed. - jackson-databind dropped from production AND test scopes. - New CI step "ADR-0008 — no direct Jackson use in production code" runs `find src/main -exec grep -l "com\\.fasterxml\\.jackson"` before ./gradlew build (unanchored — catches FQN bypass too, per Obs #1). - New CI step "ADR-0008 — Ignored.unrecognisedType is debug-only" fails the build if the method is called outside WebhookEventDispatcher (per Tomás review observation). Reviewed and approved by Tomás (Security Champion) on the PR thread. Six locked priorities verified; two non-blocking observations addressed in commit 58e6464; one operational follow-up filed for the infra side (alarm on dispatch-failed log line). 70 / 70 tests pass. ./gradlew build green end-to-end. Co-Authored-By: Rui (Tech Lead) <team-sobrado@functorful.com> Co-Authored-By: Tomás (Security Champion) <team-sobrado@functorful.com>
Summary
ARCH-08, Phase 1 — locks the architectural decision to remove direct Jackson from production code. ADR-only; ~1000-line code migration ships in follow-up MR
feat/arch-08-stripe-webhook-typed-eventsso reviewers can react to architecture before refactor lands.What's in this PR
docs/adr/0008-micronaut-serde-only.md(new) — full decision document.What this MR does NOT do (deferred to follow-up)
StripeEventhierarchy +@Serdeablerecords.WebhookEventProcessormigration offJsonNode.ReservationKeymigration offJsonNode.com.fasterxml.jackson.core:jackson-databindfrombuild.gradle..github/workflows/build.yml— fail-build ifimport com.fasterxml.jacksonappears insrc/main.(All five land together in the follow-up so the CI check turns green the same commit it's introduced.)
Three Tomás-flagged concerns — addressed in the ADR
@Serdeableprocessor generates static deserializers — noenableDefaultTyping, no@JsonTypeInfo(use=Id.CLASS)API in the surface. Default-deny on the historical Jackson CVE class.JsonNodetraversal in webhook bodies. Replaced by a typed sealedStripeEvent permits PaymentIntentSucceeded, PaymentIntentFailed, Ignoredhierarchy with per-event@Serdeablerecords. Stripe schema drift surfaces as a parse error at the boundary, not a runtimeMissingNodetraversal.stripe-java(which uses Gson), it stays inside an adapter package. Locking on Micronaut Serde now prevents three JSON libraries in one Lambda.Inventory (production code, current state)
WebhookEventProcessor.javaObjectMapper,JsonNode,MissingNodeevent/StripeWebhookEvent.javaJsonNode dataObjectreservation/ReservationKey.javaJsonNode metadatadispatch/handlers/PaymentIntentSucceededHandler.javaJsonNodedispatch/handlers/PaymentIntentFailedHandler.javaJsonNodePlus 4 test files (out of scope — CI check targets
src/mainonly).Out of scope explicitly
payment-lambdaanddata-handleralready use Micronaut Serde and don't have direct Jackson imports — verified by ADR-author inventory before drafting.revolut-webhookfollows in a sibling ADR + MR (filed as ARCH-08-followup-revolut).Test plan
./gradlew buildis unaffected. (Verified locally: build succeeds with this commit because nothing insrc/was touched.)