Skip to content

[AIT-1009] Implement Json and MsgPack serializers for path-based LiveObjects#1218

Open
sacOO7 wants to merge 6 commits into
feature/path-based-liveobjects-implementationfrom
chore/path-based-liveobjects-serialization
Open

[AIT-1009] Implement Json and MsgPack serializers for path-based LiveObjects#1218
sacOO7 wants to merge 6 commits into
feature/path-based-liveobjects-implementationfrom
chore/path-based-liveobjects-serialization

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the serialization layer for the path-based LiveObjects API (io.ably.lib.object), supporting both JSON and MessagePack encoding/decoding of object messages. This is the wire-format plumbing that sits underneath the path-based public API.

The design keeps the core lib free of any hard dependency on the optional LiveObjects plugin: the core declares interfaces and reflectively loads the implementation from the liveobjects module at runtime, returning null when the plugin is absent.

Core (lib)

  • ObjectSerializer — public interface declaring the four codec entry points (readMsgpackArray / writeMsgpackArray / readFromJsonArray / asJsonArray). The implementation is loaded reflectively and cached as a lazy singleton via a nested Holder (double-checked locking; retries until the plugin is present), exposed through the static ObjectSerializer.tryGet().
  • ObjectJsonSerializer — Gson JsonSerializer/JsonDeserializer for the protocol-message state field; delegates to the loaded ObjectSerializer, and no-ops gracefully (logs + JsonNull) when the plugin is unavailable.

Plugin (liveobjects)

  • DefaultObjectsSerializer — implements ObjectSerializer, (de)serializing arrays of object messages for JSON and MessagePack.
  • JsonSerialization — Gson setup: enum-by-code adapters and WireObjectDataJsonSerializer (handles the json-as-string encoding per OD4c5 and the "at least one value field" validation).
  • MsgpackSerialization — hand-rolled MessagePack pack/unpack for the full object model (operations, state, map/counter payloads, map entries, object data).

Wire model decoupling

The serializers operate on the WireObjectMessage model in io.ably.lib.object.message, so the new object package carries no dependency on the legacy io.ably.lib.objects package. WireObjectMessage carries the Gson annotations needed for wire-format fidelity — @SerializedName("object") on objectState (OM2g) and @JsonAdapter(WireObjectDataJsonSerializer) on WireObjectData.

Notes

  • Errors use the object package's objectStateError (ErrorInfo 500 / 92000).
  • No new test coverage for the path-based serializers yet — existing serialization tests still target the legacy package. Round-trip tests for the Wire* model are tracked as a follow-up.

Spec: OM*, OOP*, OD*, OMP*, OCN*, OST*, MCR*, MST*, MRM*, CCR*, CIN*, ODE*, MCL*

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced LiveObjectsPlugin interface to enable channel-specific live object management with lifecycle and event-handling capabilities
    • Added object serialization and deserialization support for JSON and MessagePack formats, allowing seamless data exchange
    • Implemented automatic plugin discovery via reflection, enabling live objects functionality when available on the classpath

@sacOO7 sacOO7 changed the title Implemented Json and MsgPack serializers for path based liveobjects Implemente Json and MsgPack serializers for path based liveobjects Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Introduces a LiveObjectsPlugin interface and ObjectSerializer interface in the core lib module, both using reflective plugin loading. Adds comprehensive Gson and MessagePack codecs for all LiveObjects wire protocol types (WireObjectMessage, WireObjectOperation, WireObjectState, and their nested payloads) in the liveobjects module, plus Gson annotations on WireObjectData and WireObjectMessage.

Changes

LiveObjects Wire Serialization and Plugin Interface

Layer / File(s) Summary
ObjectSerializer interface and JSON bridge (lib)
lib/.../serialization/ObjectSerializer.java, lib/.../serialization/ObjectJsonSerializer.java
ObjectSerializer declares MessagePack and JSON array contracts; its nested Holder uses double-checked locking to reflectively load DefaultObjectsSerializer, returning null on failure. ObjectJsonSerializer is a Gson adapter that delegates to the singleton, logging warnings and falling back gracefully when the plugin is absent.
LiveObjectsPlugin interface and reflective factory (lib)
lib/.../object/LiveObjectsPlugin.java
Declares the LiveObjectsPlugin interface with per-channel RealtimeObjects access, protocol message and channel state change handling, and disposal methods. The static tryInitialize delegates to a nested Factory that reflectively loads DefaultLiveObjectsPlugin, wraps AblyRealtime in an Adapter, and returns null on reflection failures.
WireObjectMessage annotations and Gson instance
liveobjects/.../message/WireObjectMessage.kt, liveobjects/.../serialization/JsonSerialization.kt
Applies @JsonAdapter(WireObjectDataJsonSerializer::class) to WireObjectData and @SerializedName("object") to WireObjectMessage.objectState. Builds a shared Gson instance with EnumCodeTypeAdapter for WireObjectOperationAction and WireObjectsMapSemantics, and defines WireObjectMessage JSON helpers and WireObjectDataJsonSerializer.
DefaultObjectsSerializer (ObjectSerializer impl)
liveobjects/.../serialization/DefaultSerialization.kt
Implements DefaultObjectsSerializer as the concrete ObjectSerializer loaded reflectively by the core lib. Provides MessagePack array read/write via readObjectMessage/writeMsgpack and JSON array read/write via toObjectMessage/toJsonObject.
MessagePack codecs for top-level wire messages
liveobjects/.../serialization/MsgpackSerialization.kt (lines 1–407)
Implements Msgpack read/write for WireObjectMessage (null-omitting field count, Gson↔Msgpack extras conversion), WireObjectOperation (action code mapping, required field validation, empty-map delete/clear consumption), and WireObjectState (required objectId/siteTimeserials/tombstone, optional nested payloads).
MessagePack codecs for map/counter/entry/data primitives
liveobjects/.../serialization/MsgpackSerialization.kt (lines 408–916)
Adds read/write codecs for all remaining wire sub-types: WireMapCreate/Set/Remove, WireCounterCreate/Inc, WireMapCreateWithObjectId, WireCounterCreateWithObjectId, WireObjectsMap, WireObjectsCounter, WireObjectsMapEntry, and WireObjectData (base64↔binary bytes, JsonParser.parseString for JSON fields).

Sequence Diagram(s)

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    Note over AblyRealtime,LiveObjectsPlugin: Plugin initialization
  end
  AblyRealtime->>LiveObjectsPlugin.Factory: tryInitialize(ablyRealtime)
  LiveObjectsPlugin.Factory->>DefaultLiveObjectsPlugin: Class.forName + constructor(AblyClientAdapter)
  DefaultLiveObjectsPlugin-->>LiveObjectsPlugin.Factory: instance or null
  LiveObjectsPlugin.Factory-->>AblyRealtime: LiveObjectsPlugin or null

  rect rgba(60, 179, 113, 0.5)
    Note over Gson,DefaultObjectsSerializer: Wire message (de)serialization
  end
  Gson->>ObjectJsonSerializer: deserialize(JsonElement, Object[])
  ObjectJsonSerializer->>ObjectSerializer.Holder: tryGet()
  ObjectSerializer.Holder->>DefaultObjectsSerializer: reflective load + cache
  DefaultObjectsSerializer-->>ObjectSerializer.Holder: ObjectSerializer
  ObjectSerializer.Holder-->>ObjectJsonSerializer: ObjectSerializer
  ObjectJsonSerializer->>DefaultObjectsSerializer: readFromJsonArray(json)
  DefaultObjectsSerializer->>JsonSerialization: toObjectMessage() per element
  JsonSerialization-->>DefaultObjectsSerializer: WireObjectMessage[]
  DefaultObjectsSerializer-->>ObjectJsonSerializer: Object[]
  ObjectJsonSerializer-->>Gson: Object[]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ably/ably-java#1216: Consumes the LiveObjectsPlugin interface introduced in this PR by wiring channel.objects in ChannelBase based on whether the plugin instance is present.

Poem

🐇 Hoppity-hop through bytes and bits,
JSON and Msgpack — the bunny knits!
A plugin loads via reflection's spell,
WireObjectMessages pack quite well.
With double-checked locks and Gson's grace,
Live Objects now have a serialized place! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: implementing JSON and MessagePack serializers for the path-based LiveObjects feature, which aligns with the primary purpose of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/path-based-liveobjects-serialization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 18, 2026 18:04 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/javadoc June 18, 2026 18:06 Inactive
- Implemented JsonSerializer annotation for better json handling
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 19, 2026 12:13 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/javadoc June 19, 2026 12:15 Inactive
Point the JSON and MsgPack serializers in io.ably.lib.object.serialization
at the new WireObjectMessage wire model instead of the legacy
io.ably.lib.objects.ObjectMessage, so the new `object` package has no
dependency on the legacy `objects` package.

- DefaultSerialization: implement the new ObjectSerializer interface and
  (de)serialize WireObjectMessage arrays (reflectively loaded via
  ObjectSerializer.Holder).
- Json/MsgpackSerialization: bind the Wire* types; replace legacy
  objectError with the object package's objectStateError (same 500/92000).
- WireObjectMessage: restore the gson annotations required for wire-format
  fidelity - @SerializedName("object") on objectState and
  @JsonAdapter(WireObjectDataJsonSerializer) on WireObjectData.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 19, 2026 12:22 Inactive
@sacOO7 sacOO7 changed the title Implemente Json and MsgPack serializers for path based liveobjects Implement Json and MsgPack serializers for path-based LiveObjects Jun 19, 2026
@sacOO7 sacOO7 changed the title Implement Json and MsgPack serializers for path-based LiveObjects [AIT-1009] Implement Json and MsgPack serializers for path-based LiveObjects Jun 19, 2026
@sacOO7 sacOO7 marked this pull request as ready for review June 19, 2026 12:23
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/javadoc June 19, 2026 12:23 Inactive
Fixes checkstyle AvoidStarImport violation on com.google.gson.*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 19, 2026 12:25 Inactive
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/path-based-liveobjects-serialization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 19, 2026 12:27 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/javadoc June 19, 2026 12:28 Inactive
@sacOO7 sacOO7 force-pushed the chore/path-based-liveobjects-serialization branch from b69d950 to bfa574f Compare June 19, 2026 12:28
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/features June 19, 2026 12:28 Inactive
@sacOO7 sacOO7 requested a review from Copilot June 19, 2026 12:29
@github-actions github-actions Bot temporarily deployed to staging/pull/1218/javadoc June 19, 2026 12:29 Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the wire-format serialization layer for the path-based LiveObjects API (io.ably.lib.object), providing JSON and MessagePack codecs for the Wire* object-message model while keeping the core lib module decoupled from the optional liveobjects plugin via reflective loading.

Changes:

  • Introduces ObjectSerializer (+ ObjectJsonSerializer) in lib, with a lazy reflectively-loaded plugin implementation.
  • Adds DefaultObjectsSerializer plus JSON/MsgPack encoding/decoding for WireObjectMessage arrays in liveobjects.
  • Updates the Wire* model with Gson annotations needed for wire-format fidelity (e.g., "object" field name and WireObjectData JSON-as-string handling).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt MessagePack (de)serialization for the path-based Wire* object model.
liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt Gson configuration + enum-by-code adapters + WireObjectData JSON-as-string adapter.
liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt Plugin-side ObjectSerializer implementation for JSON/MsgPack arrays of WireObjectMessage.
liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt Wire model updates: Gson annotations for "object" and WireObjectData adapter.
lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java Core-side serializer interface + reflective singleton loader for plugin implementation.
lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java Gson serializer/deserializer for the protocol state field via ObjectSerializer.tryGet().
lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java Core-side reflective plugin initializer interface for LiveObjects functionality.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt (1)

21-36: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Validate element types before casting object arrays.

Line 22 and Line 35 use unchecked casts from Any to WireObjectMessage; malformed upstream arrays will fail with opaque ClassCastException. A typed check gives deterministic, actionable failures.

Suggested patch
   override fun writeMsgpackArray(objects: Array<out Any>, packer: MessagePacker) {
-    val objectMessages = objects.map { it as WireObjectMessage }
+    val objectMessages = objects.mapIndexed { index, value ->
+      value as? WireObjectMessage
+        ?: throw IllegalArgumentException("Expected WireObjectMessage at index $index, got ${value::class.java.name}")
+    }
     packer.packArrayHeader(objectMessages.size)
     objectMessages.forEach { it.writeMsgpack(packer) }
   }
@@
   override fun asJsonArray(objects: Array<out Any>): JsonArray {
-    val objectMessages = objects.map { it as WireObjectMessage }
+    val objectMessages = objects.mapIndexed { index, value ->
+      value as? WireObjectMessage
+        ?: throw IllegalArgumentException("Expected WireObjectMessage at index $index, got ${value::class.java.name}")
+    }
     val jsonArray = JsonArray()
     for (objectMessage in objectMessages) {
       jsonArray.add(objectMessage.toJsonObject())
     }
     return jsonArray
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt`
around lines 21 - 36, The methods writeMsgpackArray and asJsonArray both contain
unchecked casts to WireObjectMessage without validating the input types, which
can result in opaque ClassCastException errors. Add type validation before
casting in both methods by checking if each element is an instance of
WireObjectMessage using the is operator or as? operator, and throw a descriptive
exception with the actual element type if validation fails, similar to the
pattern already implemented in readFromJsonArray where you check isJsonObject
and throw JsonParseException.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java`:
- Around line 96-105: The catch block in the plugin initialization code (around
the getDeclaredConstructor call and cast to LiveObjectsPlugin) does not handle
ClassCastException and LinkageError, which can be thrown during reflection and
type loading if the implementation exists but is incompatible. This causes the
optional LiveObjects feature to fail hard instead of gracefully degrading.
Extend the catch clause to also catch ClassCastException and LinkageError
alongside the existing exception types (ClassNotFoundException,
InstantiationException, IllegalAccessException, NoSuchMethodException,
InvocationTargetException) so the method returns null when any of these errors
occur, maintaining the resilience of this optional plugin.

In `@lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java`:
- Around line 83-90: The reflective loading of the ObjectSerializer class on
lines 84-85 can throw ClassCastException or LinkageError when the class exists
but is incompatible or incompletely linked, but these exceptions are not
currently caught in the catch block. Add ClassCastException and LinkageError to
the multi-catch exception handler in the ObjectSerializer reflective loading
section so these failures are gracefully handled the same way as other expected
exceptions, allowing the optional plugin to be disabled instead of crashing the
runtime message handling.

In
`@liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt`:
- Around line 356-407: Add validation for the required fields `siteTimeserials`
and `tombstone` in the readObjectState function. Currently these fields have
default values (empty map and false respectively), which prevents detection of
missing required fields per the WireObjectState spec. Introduce boolean flags to
track whether these fields were encountered during unpacking, then add
validation checks after the loop (similar to the existing objectId validation)
to throw an objectStateError if either siteTimeserials or tombstone is missing
from the wire payload.

---

Nitpick comments:
In
`@liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt`:
- Around line 21-36: The methods writeMsgpackArray and asJsonArray both contain
unchecked casts to WireObjectMessage without validating the input types, which
can result in opaque ClassCastException errors. Add type validation before
casting in both methods by checking if each element is an instance of
WireObjectMessage using the is operator or as? operator, and throw a descriptive
exception with the actual element type if validation fails, similar to the
pattern already implemented in readFromJsonArray where you check isJsonObject
and throw JsonParseException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c3a8978-b914-4dc6-ac31-9091b13905f6

📥 Commits

Reviewing files that changed from the base of the PR and between c8a283d and c489ac0.

📒 Files selected for processing (7)
  • lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java
  • lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java
  • lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java
  • liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt
  • liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt
  • liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt
  • liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt

Comment thread lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java
@sacOO7 sacOO7 requested a review from ttypic June 22, 2026 08:17
Base automatically changed from chore/liveobjects-add-basic-implementation to feature/path-based-liveobjects-implementation June 22, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants