fix(android): add contentful.java Entry adapter for OptimizedEntry [NT-3807] - #395
Merged
Merged
Conversation
Felipe Mamud (fmamud)
requested a review
from David Nalchevanidze (nalchevanidze)
July 31, 2026 12:37
Felipe Mamud (fmamud)
marked this pull request as ready for review
July 31, 2026 14:01
Felipe Mamud (fmamud)
requested review from
Lotfi Anwar L Arif (Lotfi-Arif),
Tim Beyer (TimBeyer) and
Charles Hudson (phobetron)
as code owners
July 31, 2026 14:01
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>
Felipe Mamud (fmamud)
force-pushed
the
NT-3807-contentful-client-compat
branch
from
August 3, 2026 08:06
cb2e053 to
2454d28
Compare
Felipe Mamud (fmamud)
requested a review
from David Nalchevanidze (nalchevanidze)
August 3, 2026 09:17
Felipe Mamud (fmamud)
requested a review
from Elton Muteswa (elton-muteswa-cf)
as a code owner
August 3, 2026 12:14
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)
force-pushed
the
NT-3807-contentful-client-compat
branch
from
August 3, 2026 12:15
4afcb3f to
f35b8b9
Compare
David Nalchevanidze (nalchevanidze)
previously approved these changes
Aug 3, 2026
Charles Hudson (phobetron)
approved these changes
Aug 3, 2026
David Nalchevanidze (nalchevanidze)
approved these changes
Aug 3, 2026
… [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>
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>
Felipe Mamud (fmamud)
force-pushed
the
NT-3807-contentful-client-compat
branch
from
August 3, 2026 14:29
d90fe0f to
76f6645
Compare
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships a
contentful.java→ Optimization SDK integration path inoptimization-androidso integrators stop hand-writing the{sys, fields, metadata}map the resolver requires. IntroducesCTEntry, an SDK-owned wrapper around aCDAEntrywhose accessors delegate to it, and typedOptimizedEntry(entry: CDAEntry, ...)overloads for Compose and Views that flow the resolved variant back throughCTEntry— read withgetField<T>/hasField/idinstead ofas?casts on a raw map. The existingMap<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 realCDAEntry; every accessor delegates:id → entry.id()contentTypeId → entry.contentType()?.id()localeCode / createdAt / updatedAt → entry.getAttribute(...)getField<T>(name) → entry.getField(name)(walks the realLocalizedResource.Localizer)hasField(name) → entry.rawFields().containsKey(name)operator get(name): String?CTEntry.from(entry: CDAEntry)— thin wrapper (CTEntry(entry)).CTEntry.from(any: Map<String, Any>, fallback = EMPTY)— fabricates aCDAEntryfrom the resolver Map via reflection (writes package-private fields with a synthetic"_"locale soLocalizedResource.getFieldresolves).CTEntry.from(json: String, fallback = EMPTY)— parses viaJSONTokener, then fabricates.MapandStringforms are fail-soft: on parse or fabrication failure they log aDiagnosticLogger.warningand return the caller-suppliedfallback(default empty) rather than throwing.toFoundation(): Map<String, Any>(delegates to the SDK's internaltoOptimizedEntryMap(entry)adapter — centralizes themetadatablock the resolver requires),toJSON(): String.ResolvedOptimizedEntry.entrychanged fromMap<String, Any>toCTEntry. Both theMapandCDAEntrycode 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.OptimizedEntry(entry: Map<String, Any>, ...)andsetEntry(entry: Map)/setContentRenderer((Map) -> View)signatures untouched — the base composable internally holds aCTEntrybut hands the callback aMapvia.toFoundation()for backwards compatibility.Adapter behavior
metadata: {tags, concepts}always emitted, populated fromCDAMetadatawhen present.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.createdAt,updatedAt,revision,locale,space,environment) surfaced when the CDA response populated them, omitted when absent — matches rawsysoptionality.description,fileName,contentType,details.{size, image.{width, height}}, andurl(not justtitle+file.url).CDARichNodere-serialized to the raw{nodeType, data, content}JSON shape. Embedded resource nodes (CDARichEmbeddedBlock/CDARichEmbeddedInline) matched before the genericCDARichHyperLinkcase, so an embedded entry no longer emitsdata.uriand loses its target; plain URI hyperlinks emitdata.uri.Datefield values ISO-8601 formatted (previously fell through toDate.toString(), unusable JSON).resolveOptimizedEntrywraps the baseline inCTEntry.from(baseline)once; the resolver-output parse passes the baselineCTEntryas fallback, so an unparseable bridge result decays to baseline content instead of a blank render.Dependency
com.contentful.java:java-sdk:10.6.0added ascompileOnly(withokhttp-jvmexcluded to avoid the okhttp5 duplicate-class conflict againstokhttp-android). Consumers that only pass entry Maps aren't forced onto it.Map/jsonCTEntry.from(...)paths write intocontentful.java's package-private fields (attrs,defaultLocale,rawFields,fields,contentType,metadata) at construction. Pinned to10.6.0; a future rename fails at runtime with a clearNoSuchFieldException.Docs
documentation/internal/sdk-knowledge/native/android.md— reconciled to the new surface (addedCTEntryto state/result types, dedicated CTEntry section describing the CDAEntry-backed storage + reflection fabrication, updatedresolveOptimizedEntryfail-soft bullet, noted the CDAEntry overload onOptimizedEntry/setEntry).CTEntry(.entry.toFoundation()where the old sample assigned.entryinto aMapvariable), added CDAEntry-overload examples, prose reflects the CDAEntry-backed shape.documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md— states that.entryis aCTEntrywrapping aCDAEntry.Tests
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 realCDAEntryaccessors, the fabricatedCDAEntryfrom a Map reads through the realLocalizer(regression guard), and malformedsys.contentTypedegrades gracefully.CDAEntryAdapterTest— whole-tree identity checks (bare entry sys/contentType shape; metadata always present whenCDAMetadatais 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 hyperlinkdata.urivs embedded-resourcedata.target)../gradlew assembleRelease— clean.:compose:assembleDebug+:views:assembleDebug) — both build cleanly against the new SDK, confirming the public API stays backwards-compatible.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
Contentful.Entry→ resolver-map mapping, aCTEntrytype on the resolver output, and a typedOptimizedEntry(entry:)initializer. This PR takes the analogous shape on Android; iOS chose an SDK-ownedCodableenvelope internally becauseContentful.Entry.init(from:)requires aLocalizationContextonly a live decode carries, while Android'sCDAEntrycan be fabricated from a Map via reflection into package-private fields — so Android delegates to a realCDAEntrydirectly rather than a Codable envelope.contentful.javaunwrapping / typedEntryMappingcompatibility, mirroring iOS's NT-3854 forEntryDecodable.🤖 Generated with Claude Code