Skip to content

fix(android): add contentful.java Entry adapter for OptimizedEntry [NT-3807] - #395

Merged
Felipe Mamud (fmamud) merged 16 commits into
mainfrom
NT-3807-contentful-client-compat
Aug 3, 2026
Merged

fix(android): add contentful.java Entry adapter for OptimizedEntry [NT-3807]#395
Felipe Mamud (fmamud) merged 16 commits into
mainfrom
NT-3807-contentful-client-compat

Conversation

@fmamud

@fmamud Felipe Mamud (fmamud) commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Ships a contentful.java → Optimization SDK integration path in optimization-android so integrators stop hand-writing the {sys, fields, metadata} map the resolver requires. Introduces CTEntry, an SDK-owned wrapper around a CDAEntry whose accessors delegate to it, and typed OptimizedEntry(entry: CDAEntry, ...) overloads for Compose and Views that flow the resolved variant back through CTEntry — read with getField<T> / hasField / id instead of as? casts on a raw map. The existing Map<String, Any> public surface is unchanged, so current consumers keep working without edits.

Fixes NT-3807.

What ships

Types

  • CTEntry (com.contentful.optimization.contentful) — wraps a real CDAEntry; every accessor delegates:
    • id → entry.id()
    • contentTypeId → entry.contentType()?.id()
    • localeCode / createdAt / updatedAt → entry.getAttribute(...)
    • getField<T>(name) → entry.getField(name) (walks the real LocalizedResource.Localizer)
    • hasField(name) → entry.rawFields().containsKey(name)
    • operator get(name): String?
    • Three factory methods normalize input to the same wrapped-CDAEntry shape:
      • CTEntry.from(entry: CDAEntry) — thin wrapper (CTEntry(entry)).
      • CTEntry.from(any: Map<String, Any>, fallback = EMPTY) — fabricates a CDAEntry from the resolver Map via reflection (writes package-private fields with a synthetic "_" locale so LocalizedResource.getField resolves).
      • CTEntry.from(json: String, fallback = EMPTY) — parses via JSONTokener, then fabricates.
    • Both Map and String forms are fail-soft: on parse or fabrication failure they log a DiagnosticLogger.warning and return the caller-supplied fallback (default empty) rather than throwing.
    • Serializers: toFoundation(): Map<String, Any> (delegates to the SDK's internal toOptimizedEntryMap(entry) adapter — centralizes the metadata block the resolver requires), toJSON(): String.
  • ResolvedOptimizedEntry.entry changed from Map<String, Any> to CTEntry. Both the Map and CDAEntry code paths wrap their output the same way; the underlying map is reachable via .entry.toFoundation() as an escape hatch.

Compose / Views surface

  • OptimizedEntry(entry: CDAEntry, ..., content: (CTEntry) -> Unit) — new Compose overload.
  • OptimizedEntryView.setEntry(entry: CDAEntry, ...) — new Views overload.
  • Existing OptimizedEntry(entry: Map<String, Any>, ...) and setEntry(entry: Map) / setContentRenderer((Map) -> View) signatures untouched — the base composable internally holds a CTEntry but hands the callback a Map via .toFoundation() for backwards compatibility.

Adapter behavior

  • metadata: {tags, concepts} always emitted, populated from CDAMetadata when present.
  • Nested resolved links (CDAEntry/CDAAsset) walked recursively; back-edges emit an unresolved Link stub via ancestor-set tracking so a real cycle doesn't recurse forever, and diamonds still fully expand on both branches.
  • Sys optional attrs (createdAt, updatedAt, revision, locale, space, environment) surfaced when the CDA response populated them, omitted when absent — matches raw sys optionality.
  • Assets carry description, fileName, contentType, details.{size, image.{width, height}}, and url (not just title + file.url).
  • CDARichNode re-serialized to the raw {nodeType, data, content} JSON shape. Embedded resource nodes (CDARichEmbeddedBlock / CDARichEmbeddedInline) matched before the generic CDARichHyperLink case, so an embedded entry no longer emits data.uri and loses its target; plain URI hyperlinks emit data.uri.
  • Date field values ISO-8601 formatted (previously fell through to Date.toString(), unusable JSON).
  • Fail-soft resolver: resolveOptimizedEntry wraps the baseline in CTEntry.from(baseline) once; the resolver-output parse passes the baseline CTEntry as fallback, so an unparseable bridge result decays to baseline content instead of a blank render.

Dependency

  • com.contentful.java:java-sdk:10.6.0 added as compileOnly (with okhttp-jvm excluded to avoid the okhttp5 duplicate-class conflict against okhttp-android). Consumers that only pass entry Maps aren't forced onto it.
  • The Map / json CTEntry.from(...) paths write into contentful.java's package-private fields (attrs, defaultLocale, rawFields, fields, contentType, metadata) at construction. Pinned to 10.6.0; a future rename fails at runtime with a clear NoSuchFieldException.

Docs

  • documentation/internal/sdk-knowledge/native/android.md — reconciled to the new surface (added CTEntry to state/result types, dedicated CTEntry section describing the CDAEntry-backed storage + reflection fabrication, updated resolveOptimizedEntry fail-soft bullet, noted the CDAEntry overload on OptimizedEntry / setEntry).
  • Compose and Views integration guides — code samples updated for CTEntry (.entry.toFoundation() where the old sample assigned .entry into a Map variable), added CDAEntry-overload examples, prose reflects the CDAEntry-backed shape.
  • documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md — states that .entry is a CTEntry wrapping a CDAEntry.

Tests

  • 73 unit tests pass. New coverage:
    • CTEntryTest — three factory methods, fail-soft with caller-supplied fallback, JSON round-trip, accessor semantics (including the JVM generic-erasure gotcha), plus three Option-A-specific tests: from(entry) reads through real CDAEntry accessors, the fabricated CDAEntry from a Map reads through the real Localizer (regression guard), and malformed sys.contentType degrades gracefully.
    • CDAEntryAdapterTest — whole-tree identity checks (bare entry sys/contentType shape; metadata always present when CDAMetadata is null; tag/concept link references; nested link expansion; back-edge cycle stub; diamond expansion; three-node cycle; self-reference; five-level linear chain; unresolved-link stub pass-through; asset happy path + no-description/no-image edge cases; asset with no file; Date field ISO-8601; URI hyperlink data.uri vs embedded-resource data.target).
  • ./gradlew assembleRelease — clean.
  • Reference implementation APKs (:compose:assembleDebug + :views:assembleDebug) — both build cleanly against the new SDK, confirming the public API stays backwards-compatible.
  • Maestro E2E on Pixel_8_API_34: Compose app 34/34 flows passed in 6m 9s (identify/reset, view/click tracking, preview-panel scenarios 1–7, live-updates locking, profile screens, refresh). Views app 30/34 passed before manual emulator shutdown aborted the final 4; the ADB stack trace right after confirms those failures are shutdown-artifact, not SDK regressions.

Notes

  • Side note: iOS covers the same surface in fix(swift): add contentful.swift Entry adapter for OptimizedEntry [NT-3808] #393Contentful.Entry → resolver-map mapping, a CTEntry type on the resolver output, and a typed OptimizedEntry(entry:) initializer. This PR takes the analogous shape on Android; iOS chose an SDK-owned Codable envelope internally because Contentful.Entry.init(from:) requires a LocalizationContext only a live decode carries, while Android's CDAEntry can be fabricated from a Map via reflection into package-private fields — so Android delegates to a real CDAEntry directly rather than a Codable envelope.
  • Related follow-up: NT-3856 tracks contentful.java unwrapping / typed EntryMapping compatibility, mirroring iOS's NT-3854 for EntryDecodable.

🤖 Generated with Claude Code

Felipe Mamud (fmamud) added a commit that referenced this pull request Aug 3, 2026
Addresses Daviti's PR feedback (#395 review comment). Mirrors iOS PR
393's identity-check style: rather than digging into the resulting Map
field-by-field with `as?` casts, each test now names the entire expected
`{sys, fields, metadata}` map and asserts against it with `assertEquals`.

Kotlin's `Map.equals` is entry-set equal (key-order-insensitive) and
`List.equals` is positional — same semantics as iOS's `JSONValue` tree
comparison in `CTEntryTests.swift`. The upside: an unexpected extra key
or shape drift anywhere in the tree now fails the test; before, only the
handful of keys a test happened to name were checked.

Adds a small set of expected-shape helpers (`sys(id, contentTypeId)`,
`linkStub(id, linkType)`, `emptyMetadata`) so each test's literal reads
as the shape it intends, not as boilerplate.

70/70 unit tests pass; release build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Felipe Mamud (fmamud) added a commit that referenced this pull request Aug 3, 2026
Addresses Daviti's PR feedback (#395 review comment). Mirrors iOS PR
393's identity-check style: rather than digging into the resulting Map
field-by-field with `as?` casts, each test now names the entire expected
`{sys, fields, metadata}` map and asserts against it with `assertEquals`.

Kotlin's `Map.equals` is entry-set equal (key-order-insensitive) and
`List.equals` is positional — same semantics as iOS's `JSONValue` tree
comparison in `CTEntryTests.swift`. The upside: an unexpected extra key
or shape drift anywhere in the tree now fails the test; before, only the
handful of keys a test happened to name were checked.

Adds a small set of expected-shape helpers (`sys(id, contentTypeId)`,
`linkStub(id, linkType)`, `emptyMetadata`) so each test's literal reads
as the shape it intends, not as boilerplate.

70/70 unit tests pass; release build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fmamud
Felipe Mamud (fmamud) force-pushed the NT-3807-contentful-client-compat branch from cb2e053 to 2454d28 Compare August 3, 2026 08:06
Felipe Mamud (fmamud) added a commit that referenced this pull request Aug 3, 2026
Addresses Daviti's PR feedback (#395 review comment). Mirrors iOS PR
393's identity-check style: rather than digging into the resulting Map
field-by-field with `as?` casts, each test now names the entire expected
`{sys, fields, metadata}` map and asserts against it with `assertEquals`.

Kotlin's `Map.equals` is entry-set equal (key-order-insensitive) and
`List.equals` is positional — same semantics as iOS's `JSONValue` tree
comparison in `CTEntryTests.swift`. The upside: an unexpected extra key
or shape drift anywhere in the tree now fails the test; before, only the
handful of keys a test happened to name were checked.

Adds a small set of expected-shape helpers (`sys(id, contentTypeId)`,
`linkStub(id, linkType)`, `emptyMetadata`) so each test's literal reads
as the shape it intends, not as boilerplate.

70/70 unit tests pass; release build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fmamud
Felipe Mamud (fmamud) force-pushed the NT-3807-contentful-client-compat branch from 4afcb3f to f35b8b9 Compare August 3, 2026 12:15
@fmamud Felipe Mamud (fmamud) changed the title fix(android): add contentful.java CDAEntry adapter for OptimizedEntry [NT-3807] fix(android): add contentful.java Entry adapter for OptimizedEntry [NT-3807] Aug 3, 2026
Felipe Mamud (fmamud) and others added 5 commits August 3, 2026 16:29
… [NT-3807]

Ships a `CDAEntry -> OptimizedEntry` conversion path so integrators stop
hand-writing the `{sys, fields, metadata}` mapping: `CDAEntry.toOptimizedEntryMap()`,
new `OptimizedEntry(entry: CDAEntry, ...)` Compose overload and
`OptimizedEntryView.setEntry(entry: CDAEntry)` / `setResolvedContentRenderer(...)`
View overloads, and typed accessors on the existing `ResolvedOptimizedEntry`
(`id`, `contentTypeId`, `getField<T>`, `getEntry`, `getEntries`, `getAsset`) plus
a `ResolvedAsset` peer.

Metadata is always emitted (`{tags, concepts}`) so the resolver's entry guard
cannot silently fall back to baseline. Asset mapping surfaces `description`,
`fileName`, `contentType`, and `details.{size, image.{width, height}}` instead
of only `title` / `file.url`. Rich Text URI hyperlinks are emitted with
`data.uri` and split from embedded resource nodes (`data.target`). `Date`
field values are ISO-8601 formatted instead of falling through to
`Date.toString()`.

`contentful.java:java-sdk:10.6.0` is added as `compileOnly` (with `okhttp-jvm`
excluded to avoid the okhttp5 duplicate-class conflict with `okhttp-android`),
so consumers that only pass entry Maps are not forced to depend on it.

Existing `OptimizedEntry(entry: Map<String, Any>, ...)` and
`setContentRenderer((Map<String, Any>) -> View)` signatures are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removed prose explaining what code already says. Kept only the non-obvious
"why" notes: cycle-guard rationale on entryToMap, embedded-node ordering
in richNodeToMap, okhttp-jvm exclusion, JVM lambda-erasure explainer on
setResolvedContentRenderer, and the reflection fixture note.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the scenarios OptimizationEntryMappingTests.swift covers:

- Sys optional attrs: createdAt, updatedAt, revision, locale, space,
  environment. Emitted when the CDA response populates them (via
  CDAResource.getAttribute) and omitted when absent — matches the
  optionality of the raw sys block.
- Link resolution: diamond expansion, 3-node cycle, self-referencing
  entry, five-level linear chain, and a raw unresolved link stub inside
  a field.
- Asset edge cases: no description → key omitted; non-image file →
  details.image omitted; asset with no file → sys and title preserved
  with an empty file record.
- Rich Text: plain URI hyperlink emits data.uri and content (proving the
  branch ordering fix — embedded resource cases match before the generic
  hyperlink case, so a plain hyperlink no longer emits data.target).

New RawEntryMapPassThroughTest confirms the "raw entries" path: a
hand-built Map<String, Any> flows through the base OptimizedEntry(Map)
overload and reads through ResolvedOptimizedEntry's typed accessors —
no contentful.java dependency needed at runtime for that integration.

64/64 tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Compose OptimizedEntry(CDAEntry, ...) content callback now receives a
  CDAEntry (the winning variant, or the baseline as fallback), not a
  ResolvedOptimizedEntry. The variant CDAEntry is already reachable
  through the baseline's nt_experiences → nt_variants link graph — no
  decoder/parser needed. Adds a public findVariantEntry(baseline, id)
  helper.
- Drop toOptimizedEntryMap as an extension on CDAEntry in favor of a
  standalone toOptimizedEntryMap(entry: CDAEntry).
- Remove setResolvedContentRenderer from OptimizedEntryView (unused).
- Revert ResolvedOptimizedEntry's typed accessors and ResolvedAsset now
  that no SDK caller needs them.
- Drop the corresponding accessor / raw-map pass-through test files; add
  4 findVariantEntry tests to CDAEntryAdapterTest.

58/58 tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a CTEntry class that wraps the resolver's `{sys, fields, metadata}`
map, matching the design of iOS's CTEntry in PR 393. Three constructors
normalize the input to the same internal shape: from(CDAEntry) via the
existing toOptimizedEntryMap; from(any: Map) with an org.json round-trip
for JSON-safety normalization; from(json: String) via JSONTokener. Both
Map- and JSON-based constructors accept a `fallback: CTEntry` (defaulting
to empty), log a warning through DiagnosticLogger on a parse failure,
and return the fallback instead of throwing.

ResolvedOptimizedEntry.entry changes from Map<String, Any> to CTEntry.
OptimizationClient.resolveOptimizedEntry wraps the baseline once and
passes it as the fallback for the resolver-output parse — an
unparseable bridge result now decays to the mapped baseline (baseline
content renders) rather than an empty entry.

Compose OptimizedEntry(CDAEntry, ...) now hands the content callback a
CTEntry built from the resolver's output map (no more findVariantEntry
graph walk; the CTEntry represents the winning variant regardless of
whether it's reachable via nt_experiences → nt_variants). The base
OptimizedEntry(Map, ...) overload is untouched — its Map-based callback
still fires via `result.entry.toFoundation()`. Same story for
OptimizedEntryView: internal state is CTEntry, public setContentRenderer
signature stays (Map) -> View.

CDAEntry.toOptimizedEntryMap is now internal (only CTEntry.from(CDAEntry)
needs it). findVariantEntry is removed along with its 4 tests. New
CTEntryTest (13 tests) covers accessors, three constructors, fallback
behavior, and JSON round-trip.

Validation:
- unit tests: 70/70 pass
- release build clean
- both reference impl APKs assemble cleanly
- Maestro E2E (Pixel_8_API_34): Compose 34/34 pass, Views 30+/34 pass
  before manual emulator shutdown aborted the remaining flows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Felipe Mamud (fmamud) and others added 11 commits August 3, 2026 16:29
Removed section headers, restatements of what code says, and rationale
prose that's redundant with clear method/test names. Kept only the
"why" notes a future reader can't recover from the code alone:
- ancestor cycle rationale on entryToMap
- embedded-node-before-hyperlink ordering in richNodeToMap
- CTEntry.from fallback docstring (one line each)
- okhttp-jvm exclusion rationale
- JVM generic-erasure gotcha in the getField test
- reflection-fixture note explaining why we set private fields

70/70 tests still pass. -101 lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates documentation/internal/sdk-knowledge/native/android.md to reflect:

- CTEntry added to the state/result types row with source pointer.
- OptimizedEntry / OptimizedEntryView table rows note the CDAEntry
  overload and its CTEntry callback (Compose) or Map renderer (Views).
- resolveOptimizedEntry fail-soft bullet: baseline is wrapped via
  CTEntry.from(baseline), the resolver-output parse passes the baseline
  CTEntry as fallback, and ResolvedOptimizedEntry.entry is a CTEntry.
- selectedOptimizations semantics bullet mentions the setEntry(CDAEntry)
  overload alongside the Map one.
- New CTEntry section documents the three factory methods, fail-soft
  behavior with DiagnosticLogger warnings, serializers, and mirrored
  CDAEntry accessors.
- New OptimizedEntry(CDAEntry) / setEntry(CDAEntry) section documents
  the overload routing and the compileOnly contentful.java dep with the
  okhttp-jvm exclusion.

pnpm knowledge:check passes; every new source pointer resolves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Guides and the Android runtime concept doc referenced
ResolvedOptimizedEntry.entry as a Map<String, Any>; it's now a CTEntry.
Fixes the load-bearing prose and one broken assignment in the Compose
guide's DirectResolution pattern, adds CDAEntry-overload notes to both
guides, and clarifies the CTEntry shape in the concept doc.

Compose guide: fix `resolvedEntry = ....entry` to `.entry.toFoundation()`;
add a CDAEntry overload example alongside the base Map example; update
the ResolvedOptimizedEntry description to name CTEntry and its accessors.

Views guide: name CTEntry in the ResolvedOptimizedEntry description,
mention the typed `setEntry(CDAEntry)` overload, unwrap `.entry` via
`.toFoundation()` in the direct-resolution snippet.

Concept doc: state that `.entry` is a CTEntry and reference its
accessors + `toFoundation()` escape hatch.

pnpm knowledge:check passes; format:check clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses Daviti's PR feedback (#395 review comment). Mirrors iOS PR
393's identity-check style: rather than digging into the resulting Map
field-by-field with `as?` casts, each test now names the entire expected
`{sys, fields, metadata}` map and asserts against it with `assertEquals`.

Kotlin's `Map.equals` is entry-set equal (key-order-insensitive) and
`List.equals` is positional — same semantics as iOS's `JSONValue` tree
comparison in `CTEntryTests.swift`. The upside: an unexpected extra key
or shape drift anywhere in the tree now fails the test; before, only the
handful of keys a test happened to name were checked.

Adds a small set of expected-shape helpers (`sys(id, contentTypeId)`,
`linkStub(id, linkType)`, `emptyMetadata`) so each test's literal reads
as the shape it intends, not as boilerplate.

70/70 unit tests pass; release build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
iOS PR 393's `CTEntry` holds a Codable `CDA.Entry` struct (sys/fields/metadata
as typed properties) built once from `Contentful.Entry` at construction, not
the raw dictionary — its docstring says "shares the resolved shape with Entry,
not the type." This aligns Android on the same shape: replaces the internal
`Map<String, Any>` with a typed `Envelope` data class (`sys: Sys?`, `fields:
Map<String, Any>`, `metadata: Metadata?`), reconstructed from the resolver
Map at `from(Map/JSON/CDAEntry)` and back to a Map via `toFoundation()` for
consumers and the bridge.

Public API is unchanged. `id`, `contentTypeId`, `localeCode`, `createdAt`,
`updatedAt`, `getField<T>`, `hasField`, `operator get(name)`, `toFoundation`,
`toJSON`, and the three `from(...)` factories all read/write the same shape
they did before — internal accessors now walk typed properties instead of
`as?` casts on nested Maps.

Sys carries its explicit typed fields (id/type/contentTypeId/createdAt/
updatedAt/revision/locale) plus an `extras` bag so a resolver Map with e.g.
`space`/`environment` link refs round-trips through the envelope unchanged.

70/70 unit tests pass; release build clean; both reference APKs
(:compose:assembleDebug, :views:assembleDebug) still build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Full delegation to contentful.java's CDAEntry: CTEntry stores a
private val entry: CDAEntry and every accessor calls through
(id → entry.id(); contentTypeId → entry.contentType()?.id();
localeCode/createdAt/updatedAt → entry.getAttribute(...);
getField<T> → entry.getField(name); hasField → entry.rawFields().containsKey).
from(entry: CDAEntry) is a thin wrapper; from(any: Map) and from(json)
fabricate a CDAEntry via reflection so downstream reads always go
through real contentful.java accessors.

Public API unchanged. toFoundation()/toJSON() still use the SDK adapter
so the resolver Map shape is preserved for the JS bridge.

Tradeoff: production code now reflects into contentful.java's
package-private fields (attrs, defaultLocale, rawFields, fields,
contentType, metadata, tags, concepts) on the Map/JSON paths. Pinned to
contentful.java 10.6.0; a future rename fails at runtime with a clear
NoSuchFieldException.

New Option A-specific tests:
- from(entry) reads through the real CDAEntry accessors
- fabricated CDAEntry from a Map reads through the real CDAEntry accessors
  (regression guard for the Localizer shape)
- fabricated entry with a bad contentType Map falls back to null

73/73 unit tests pass; release build clean; both reference APKs
(:compose:assembleDebug, :views:assembleDebug) still build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to `refactor(android): back CTEntry with a real CDAEntry
(Option A)` — CTEntry is no longer an envelope wrapping the resolver
Map; it wraps a real CDAEntry and delegates every accessor. Docs
still called it "the SDK-owned typed view over the entry map."

- android.md KB — the CTEntry section now describes the wrapper shape,
  the three factory methods (from(CDAEntry) trivial; from(Map)/from(json)
  fabricate a CDAEntry via reflection into package-private fields), and
  the pinned 10.6.0 dependency contract.
- Compose + Views integration guides — reword the "typed view over the
  entry map" to "wrapper around a CDAEntry whose accessors delegate to
  it". No code sample changes.
- Runtime concept doc — same reword.

pnpm knowledge:check passes; all source pointers still resolve.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops all reflection into contentful.java's package-private fields.
CTEntry now holds a nested `CTEntry.Entry` data class (SDK-owned, with
typed Sys/Metadata/Link sub-classes) that Gson serializes natively.
Three factories, all reflection-free:

- from(entry: CDAEntry): walks the live CDAEntry via the existing
  toOptimizedEntryMap adapter and Gson-decodes the result into Entry.
- from(any: Map): rejects self-referential input up-front via a
  hand-rolled ancestor-set cycle check (Gson's serializer would blow
  its stack otherwise), then round-trips through Gson.
- from(json: String): decodes directly with gson.fromJson.

EMPTY is a plain Entry(sys=null, fields=emptyMap(), metadata=null).
fabricateEntry, fabricateContentType, fabricateMetadata, localizeFields,
setPrivateField, FABRICATED_LOCALE and the org.json helper cluster all
gone from production.

Renames toFoundation() to toMap() to fit Kotlin idioms; toJSON pairs
with toMap the way toList pairs with toSet. Four SDK call sites and
five doc mentions updated to match.

ISO-8601 formatting: switched Locale.US to Locale.ROOT in both
CTEntry.kt and CDAEntryAdapter.kt, and wrapped CDAEntryAdapter's
formatter in ThreadLocal (fixes a latent race where multiple
coroutine dispatchers could hit the shared SimpleDateFormat).

New JSON-identity tests exercise the full String → CTEntry → toJSON
round-trip and compare parsed trees structurally (order-insensitive
on objects, positional on arrays, numeric equality across Int/Double
so Gson's default number widening doesn't false-positive).

Validation:
- 75/75 unit tests pass
- SDK assembleRelease clean
- Reference APKs (:compose:assembleDebug, :views:assembleDebug) clean
- pnpm knowledge:check passes (1295 source pointers resolve)
- Prettier clean on the four touched docs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prior docs described the CTEntry-wrapping-CDAEntry-with-reflection
design. After the envelope refactor, CTEntry is backed by an SDK-owned
`CTEntry.Entry` data class that Gson serializes natively — no
reflection, no wrapping of a live CDAEntry.

- android.md KB section rewritten: dropped the "fabricates CDAEntry via
  reflection" claim, described the SDK-owned Entry data class shape and
  the Gson round-trip, kept the `getField<T>` returns-Double-on-JSON-boundary
  note that iOS has too. Source pointer chain updated.
- Compose + Views integration guides: reword "SDK-owned wrapper around
  a CDAEntry" to "SDK-owned view over the resolved entry" and list the
  full accessor surface (`id`, `getField<T>`, `hasField`, `contentTypeId`,
  `createdAt`, `updatedAt`, `localeCode`, `toMap()`).
- Runtime concept doc: same reword.

pnpm knowledge:check passes; Prettier clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…y tests as JSON identity

CTEntry now owns the full JSON round-trip (`from(CDAEntry)` / `from(Map)` / `from(json)`
→ `toMap()` / `toJSON()`), so the standalone `CDAEntryAdapter` and its test file are dead.
Rewrite `CTEntryTest` around `assertJsonIdentity`: parse wire JSON, wrap in a `CTEntry`,
serialize back, and compare parsed trees — with zero reflection, in production or tests.
The `Entry.from(CDAEntry)` walk stays covered by the reference-implementation E2E, which
is the only place a real `CDAEntry` graph exists.

Docs: correct the SDK-knowledge entry to say `fields: Map<String, JSONValue>` and note
the `JSONValueTypeAdapter` + reflection-free stance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CTEntry.kt initializes a top-level `Gson` instance and reads
`com.google.gson.*` from a Kotlin file-scope val, so the class-init
runs the first time anything touches CTEntry. contentful.java (which
transitively brings Gson) is declared `compileOnly` so consumers who
never touch CDAEntry aren't forced onto it — but that means Gson was
absent from the app's runtime classpath, and the compose/views
reference apps (both Map-only consumers) crashed with
`java.lang.NoClassDefFoundError: com/google/gson/GsonBuilder` at
CTEntry's <clinit> during first render, taking down every E2E flow
that got past `fab-visible`.

Add Gson as a direct `implementation` dep on the SDK so the runtime
classpath has it regardless of whether the consumer imports
contentful.java. contentful.java stays compileOnly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fmamud
Felipe Mamud (fmamud) force-pushed the NT-3807-contentful-client-compat branch from d90fe0f to 76f6645 Compare August 3, 2026 14:29
@fmamud
Felipe Mamud (fmamud) merged commit 525c55a into main Aug 3, 2026
67 of 68 checks passed
@fmamud
Felipe Mamud (fmamud) deleted the NT-3807-contentful-client-compat branch August 3, 2026 14:50
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.

3 participants