Skip to content

LiveObjects: conform JSONValue to CustomStringConvertible for readable, deterministic JSON output #2245

Description

@sacOO7

Problem statement

JSONValue is the type LiveObjects users meet first: it's what compactJson() returns, and printing it is the very first thing a quickstart reader does after fetching an object's state. But JSONValue declares no CustomStringConvertible conformance, so print(json) and string interpolation fall back to Swift's reflection-based rendering:

let json = try reactionsMap.compactJson() // logically {"likes": 10, "hearts": 0}
print("Reactions updated: \(json)")

prints:

Reactions updated: object(["hearts": AblyLiveObjects.JSONValue.number(0.0), "likes": AblyLiveObjects.JSONValue.number(10.0)])

This has three problems:

  1. It's noise. The type-name prefixes and enum-case wrappers bury the actual data. Nobody reading a console wants AblyLiveObjects.JSONValue.number(10.0) where 10 would do.
  2. It's nondeterministic. Dictionary iteration order makes the printed key order vary run to run, so the same state prints differently each time.
  3. It's inconsistent with the other Ably SDKs. In ably-java, compactJson() hands back a gson JsonObject whose toString() prints compact JSON text; in ably-js, console.log shows the plain object. Swift is the only SDK where printing the compact JSON doesn't give you something that looks like JSON — and the only one where the documentation's // console: example outputs can't literally match what the user sees.

There's also no public escape hatch: the JSONValue → Foundation bridge (toJSONSerializationInput) is internal, so a user who wants readable output has to hand-write a recursive renderer over the enum.

Proposed solution

Conform JSONValue to CustomStringConvertible, rendering compact JSON text, with object keys sorted for deterministic output:

extension JSONValue: CustomStringConvertible {
    /// Compact JSON text, with object keys sorted for deterministic output.
    public var description: String { ... }
}

With that in place, the example above prints:

Reactions updated: {"hearts":0,"likes":10}

Notes on the shape of the change:

  • Additive and non-breaking: a conformance on a type this SDK owns, no signature changes.
  • The implementation can reuse the existing internal serialization machinery, or a small recursive renderer over the six cases; sorted keys keep the output stable.
  • Optionally also conform CustomDebugStringConvertible so LLDB's po gives the same readable form.

Why this helps beyond nicer printing

  • Debugging: po json in LLDB and print in the console immediately show the state you actually care about, instead of enum plumbing. That matters most in exactly the situations where LiveObjects users reach for compactJson() — inspecting what the synchronized state currently is.
  • Logging: interpolating a JSONValue into any log line produces compact, greppable JSON.
  • Testing: deterministic output (sorted keys) means test assertions and snapshot comparisons against printed state are reproducible.
  • Documentation accuracy: the quickstart and reference examples show outputs like // console: {"likes":10.0,"hearts":0.0}. Today those are a logical representation rather than what Swift actually prints; with this conformance they become literally true, matching the behavior ably-java and ably-js users already get.

References

┆Issue is synchronized with this Jira Task by Unito

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions