From e4bdfe60d30e64d9c716e75ec1da42b08ac06b0c Mon Sep 17 00:00:00 2001 From: Chris Roadfeldt Date: Sat, 27 Jun 2026 10:09:10 -0500 Subject: [PATCH] adr-018: snake_case wire/events (AEP conformance; reverses camelCase draft) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revises ADR-018 from camelCase to snake_case for the wire and event payloads, to match the UDLM data-model casing reversal (UDLM naming-conventions §4) and conform to AEP (aep.dev, the dcm-project team's adopted API standard, snake_case). UDLM is consumed natively (canonical data model) → the model IS the wire form → one casing; the AEP-bound API forces snake_case. Native consumption + AEP jointly settle it. The camelCase first draft was based on research that hadn't accounted for AEP. - Wire/event payloads snake_case; Go uses snake json tags; Python Pydantic maps attribute→wire directly (the `alias_generator=to_camel` requirement is removed). - CloudEvents envelope + dot-notation topics unchanged (orthogonal to payload casing). - Casing only changes at an export adapter to a foreign domain (e.g. K8s CRD). - adr/README.md row updated. (Corroboration: DISCUSSION-TOPICS records the dcm-project team's own established pattern as snake_case — the reversal aligns us.) Companion to croadfeldt/udlm#11. Co-Authored-By: Claude Opus 4.8 --- ...18-wire-serialization-event-conventions.md | 36 ++++++++++--------- architecture/adr/README.md | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/architecture/adr/018-wire-serialization-event-conventions.md b/architecture/adr/018-wire-serialization-event-conventions.md index 6d56d66..911e838 100644 --- a/architecture/adr/018-wire-serialization-event-conventions.md +++ b/architecture/adr/018-wire-serialization-event-conventions.md @@ -2,41 +2,43 @@ **Status:** Accepted **Date:** June 2026 -**Docs:** ADR-003 (Four Lifecycle States), ADR-009 (API Gateway & Control Plane); UDLM `registry/naming-conventions.md` §4; CNCF CloudEvents -**Tracking:** companion to the UDLM data-model casing decision (naming-conventions §4) +**Docs:** ADR-003 (Four Lifecycle States), ADR-009 (API Gateway & Control Plane); UDLM `registry/naming-conventions.md` §4; AEP (`aep.dev`); CNCF CloudEvents +**Tracking:** companion to the UDLM data-model casing decision (naming-conventions §4). Supersedes the camelCase first draft of this ADR. ## Context -UDLM defines the **data model** and fixes its on-the-wire casing — **`camelCase` keys** (UDLM `naming-conventions.md` §4): UDLM is an API- and event-bus-driven model consumed by code, not hand-edited config. DCM is the **runtime** that serializes and transports that model — over REST/gRPC at the API gateway (ADR-009) and over the event bus — between services written in **Go and Python**. This ADR covers the DCM-side conventions so the camelCase contract flows end to end without per-hop key-translation layers. (The casing of the data model itself is UDLM's call; this ADR is how the runtime honors it.) +UDLM defines the **data model** and fixes its on-the-wire casing — **`snake_case` keys** (UDLM `naming-conventions.md` §4). DCM is the **runtime** that serializes and transports that model — over REST/gRPC at the API gateway (ADR-009) and over the event bus — between services written in **Go and Python**. This ADR covers the DCM-side conventions so the snake_case contract flows end to end without per-hop key-translation layers. (The casing of the data model itself is UDLM's call; this ADR is how the runtime honors it.) + +**Why snake_case (the reversal):** UDLM is a **canonical data model consumed natively** — the model *is* the wire form, so there is no separate "API casing" to translate to. DCM's API is the consumer with a hard external constraint: it conforms to **AEP** (`aep.dev`, the dcm-project engineering team's adopted API-design standard, enforced by `aep-dev/aep-openapi-linter`), whose prescribed fields are snake_case (`page_size`, `*_time`). Native-universal consumption **+** AEP-bound API jointly force one casing — snake_case. The first draft of this ADR chose camelCase on research that had not accounted for AEP; this revision reverses it. (Empirically: the AEP linter reports zero casing findings against the existing snake_case OpenAPI specs.) ## Decision -1. **Wire payloads are `camelCase`** — request/response bodies and event payloads match the UDLM data model. No snake_case↔camelCase translation layer between the API, the event bus, and services. +1. **Wire payloads are `snake_case`** — request/response bodies and event payloads match the UDLM data model and AEP. No snake_case↔camelCase translation layer between the API, the event bus, and services. -2. **Go services:** PascalCase exported struct fields with explicit tags — `json:"camelCase" yaml:"camelCase"`. Go keeps its idiom; the wire stays camelCase. +2. **Go services:** PascalCase exported struct fields with explicit tags — `json:"snake_case" yaml:"snake_case"`. Go keeps its idiom; the wire stays snake_case. ```go type ResourceDefinition struct { - ResourceID string `json:"resourceId" yaml:"resourceId"` - MemoryLimit string `json:"memoryLimit" yaml:"memoryLimit"` + ResourceID string `json:"resource_id" yaml:"resource_id"` + MemoryLimit string `json:"memory_limit" yaml:"memory_limit"` } ``` -3. **Python services:** native `snake_case` attributes via **Pydantic** with `alias_generator=to_camel` + `populate_by_name=True`; serialize `by_alias=True`. Python keeps PEP 8; the wire stays camelCase. +3. **Python services:** native `snake_case` attributes via **Pydantic** — attribute names map **directly** to wire keys, so no alias generator is needed (the camelCase draft required `alias_generator=to_camel`; that is now removed). Python keeps PEP 8 *and* the wire matches it. -4. **Events use the CloudEvents envelope** (CNCF). Event **payload** property keys are `camelCase`. Event **type identifiers / topics** are lowercase **dot notation** — `resource.discovered`, `entity.realized`, `resource.definition.created` — so brokers (Kafka/NATS/etc.) can **wildcard-route** (`resource.definition.*`). Topics are never camelCased. +4. **Events use the CloudEvents envelope** (CNCF). Event **payload** property keys are `snake_case`. Event **type identifiers / topics** are lowercase **dot notation** — `resource.discovered`, `entity.realized`, `resource.definition.created` — so brokers (Kafka/NATS/etc.) can **wildcard-route** (`resource.definition.*`). Topics are an event-naming concern, orthogonal to payload casing. (CloudEvents *context attributes* are themselves flatcase per the CloudEvents spec — neither snake nor camel — and are unaffected.) -5. **No ad-hoc translation.** Frontends, third-party webhooks, and microservices consume the same camelCase shape; serialization config is centralized (Go tags / Pydantic aliasing), not reinvented per service. +5. **No ad-hoc translation.** Frontends, third-party webhooks, and microservices consume the same snake_case shape; serialization config is centralized (Go tags), not reinvented per service. The one place casing changes is an **export adapter** at a foreign-domain boundary (e.g. projecting a resource into a Kubernetes CRD, which is camelCase by convention) — never inside the DCM/UDLM/DAV core. ## Options considered -- **snake_case on the wire** — rejected: forces manual `json:"snake_case"` tags on every Go field anyway, and adds friction for any web/UI/webhook consumer. -- **PascalCase keys** — rejected: legacy CloudFormation idiom only; unnatural for the Go/Python/JS consumers here. -- **camelCase topics** — rejected for event *names*: breaks broker wildcard routing; dot-notation is the broker-friendly idiom. +- **camelCase on the wire** — rejected: conflicts with AEP (the adopted, lint-enforced API standard) and with native-universal UDLM consumption; would re-introduce the translation layer native consumption exists to remove. (This was the first draft; reversed.) +- **PascalCase keys** — rejected: legacy CloudFormation idiom only. +- **camelCase topics** — moot: event names use dot-notation regardless of payload casing, for broker wildcard routing. ## Consequences -- Frontends and third-party webhook consumers ingest payloads directly — no key-translation layer. -- Fewer custom serialization configs across microservices; one convention from API request → event bus → service. +- API specs are AEP-conformant on casing; the `aep-openapi-linter` casing checks pass. +- Python services need **no** Pydantic alias generator — attribute = wire key. Go adds snake_case struct tags (mechanical, centralized). +- Frontends and third-party webhook consumers ingest payloads directly — no key-translation layer; one convention from API request → event bus → service. - Events are CloudEvents-compliant; dot-notation topics enable wildcard subscriptions. -- Go and Python each keep their native idioms; the boundary mapping is mechanical (struct tags / Pydantic). -- This ADR is the DCM realization of UDLM `naming-conventions.md` §4 — the data-model casing is owned by UDLM; the transport/serialization conventions are owned here. +- This ADR is the DCM realization of UDLM `naming-conventions.md` §4 — the data-model casing is owned by UDLM; the transport/serialization conventions are owned here. Both are now snake_case, so the contract is identity end to end. diff --git a/architecture/adr/README.md b/architecture/adr/README.md index 4d08c07..b87cc1e 100644 --- a/architecture/adr/README.md +++ b/architecture/adr/README.md @@ -23,4 +23,4 @@ Short, reviewable summaries of the major architectural decisions in DCM. Each AD | [015](015-minimal-infrastructure.md) | Minimal Infrastructure | PostgreSQL is the only required dependency; everything else is optional | | [016](016-application-definition-language.md) | Application Definition Language | **OPEN** — How should consumers define multi-resource applications? Options under evaluation | | [017](017-brownfield-greening-discovered-ingestion.md) | Brownfield Greening / Discovered Ingestion | Bring existing resources into the four states — two discovery avenues (provider / 3rd-party), Discovered store holds unclaimed, reverse placement → claim → Realized, optional Intent backport; correlation IDs dedup the same resource across sources | -| [018](018-wire-serialization-event-conventions.md) | Wire Serialization & Event Conventions | camelCase payloads end-to-end (Go json tags / Python Pydantic to_camel); CloudEvents envelope; event topics use lowercase dot-notation for broker wildcard routing — the runtime side of UDLM's data-model casing | +| [018](018-wire-serialization-event-conventions.md) | Wire Serialization & Event Conventions | snake_case payloads end-to-end (AEP-conformant; Go json tags, Python Pydantic native — no alias generator); CloudEvents envelope; event topics use lowercase dot-notation for broker wildcard routing — the runtime side of UDLM's data-model casing |