Skip to content

serde_json/arbitrary_precision in workspace deps breaks downstream serde-derived deserializers #4989

@KaiserKarel

Description

@KaiserKarel

Summary

The workspace's serde_json declaration in Cargo.toml enables the arbitrary_precision feature:

serde_json = { version = "1.0.128", features = ["raw_value", "arbitrary_precision"] }

This feature is non-additive in practice. Cargo unifies features across the whole dependency graph, so any consumer of any spacetimedb crate (sdk, lib, sats, schema, …) ends up with arbitrary_precision enabled on their serde_json — even crates that have nothing to do with SpacetimeDB.

What breaks

With arbitrary_precision on, serde_json's parser emits every JSON number as a struct of the form:

{"$serde_json::private::Number": "..."}

…instead of via visit_f64 / visit_i64 / etc. Inside serde's internally-tagged-enum buffer-replay path (#[serde(tag = "...")], which buffers the entire JSON object into Content<'de> to dispatch on the discriminator), this means every numeric field becomes Content::Map(...) rather than Content::F64/I64/etc.

When the variant payload is then replayed into the field's expected type, deserialization fails with errors like:

invalid type: map, expected f64

(or i32/i64/u64 — whichever numeric field is encountered first).

Concrete impact

I'm a downstream user (Rust GUI app) that uses both spacetimedb-sdk and tdlib-rs (Telegram bindings). tdlib-rs makes heavy use of #[serde(tag = "@type")] enums for Telegram's discriminated-union wire format, and has many numeric (f64, i32, i53→i64) fields. Adding spacetimedb-sdk as a dep causes every getChat / searchChatMessages / etc. response to fail deserialization at the first numeric field reached through a tagged enum boundary.

A trimmed reproduction:

# Cargo.toml of any non-spacetimedb crate
[dependencies]
spacetimedb-sdk = "2"   # or any spacetimedb crate
my-other-crate = "*"    # uses #[serde(tag = "...")] + numeric fields
// my-other-crate fails at runtime even though its source didn't change
serde_json::from_str::<TaggedEnumWithNumericFields>(payload)
//   -> Err("invalid type: map, expected f64")

Why it's a footgun

arbitrary_precision is a whole-program feature flag. Every crate that compiled cleanly without spacetimedb in the workspace can silently break when spacetimedb is added — no source change in the broken crate, no compile error, just runtime deserialization failures.

Per tracing of feature unification:

Without spacetimedb:
  serde_json: ['alloc', 'default', 'indexmap', 'preserve_order', 'raw_value', 'std']

With spacetimedb-sdk added:
  serde_json: ['alloc', 'arbitrary_precision', 'default', 'indexmap', 'preserve_order', 'raw_value', 'std']
                       ^^^^^^^^^^^^^^^^^^^^^^^

This is similar in spirit to the serde_with base64-feature breakage and other non-additive features. The serde-rs/json maintainers warn against using arbitrary_precision in libraries for exactly this reason (serde-rs/json#852, #1033).

What I checked

I grepped the SpacetimeDB code for actual usages of arbitrary-precision-specific serde_json APIs — serde_json::Number::from_string_unchecked, big-decimal preservation patterns, Value-based pre-parse with custom Number handling — and didn't find any in crates/sats, crates/lib, crates/bindings, or crates/sdk. The only serde_json::Number usage is Number::from(3u8) in crates/cli/src/spacetime_config.rs, which doesn't require the feature. RawValue is used (separate raw_value feature, kept). Pretty-printing in CLI doesn't need it either.

So arbitrary_precision looks like a leftover feature that wasn't strictly required.

Fix

Drop the feature from the workspace declaration:

-serde_json = { version = "1.0.128", features = ["raw_value", "arbitrary_precision"] }
+serde_json = { version = "1.0.128", features = ["raw_value"] }

If a sub-crate genuinely needs arbitrary-precision number round-tripping (e.g. for U256/I256 wire decoding), opt it in locally rather than pushing it on every consumer's serde_json.

I've been running the diff locally for a few hours against tdlib-rs-heavy code paths with no functional regressions in the SpacetimeDB module I publish.

Workaround for downstream users

Until this lands upstream, downstream users can:

[patch.crates-io]
spacetimedb = { git = "...", branch = "no-arbitrary-precision" }
spacetimedb-sdk = { git = "...", branch = "no-arbitrary-precision" }
# … one entry per spacetimedb-* crate the workspace pulls in

…pointing at a fork with arbitrary_precision removed. (See https://github.com/fifteenlabs/SpacetimeDB/tree/fifteen/no-arbitrary-precision for an example.)

Happy to send a PR if useful.

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