Skip to content

Serialization builds an intermediate owned JSON tree #2

Open

Description

@cblanquera

Timing: Follow-up after trial/3 merges into main; defer implementation until then.

Review baseline: Local trial/3 at a094418baf6388e3eeca23821df481a7f0a8751c. This branch is not yet on the remote. Source locations below refer to that snapshot and should be rechecked after merge.

Problem

Value::serialize converts into a complete owned serde_json::Value before invoking the requested serializer, copying JSON-compatible strings and containers. Nest::path_as similarly constructs owned JSON before typed deserialization. These paths can impose avoidable traversal and allocation costs; they are not established correctness failures.

Evidence

  • crates/lib/src/data/value.rs:632-639: self.to_json()?.serialize(serializer).
  • crates/lib/src/data/nest.rs:417-425: rich value → owned JSON → typed value.

Acceptance criteria

  • Review direct Serde serialization/deserialization adapters to avoid unnecessary intermediate trees where practical.
  • Preserve finite budgets, nested-path diagnostics, insertion order, missing/null semantics, and explicit rejection of unsupported native/date/byte/file values.
  • Preserve the current prevalidation/error contract; do not silently change partial-output behavior.
  • Verify wire compatibility and error paths, and document allocation/performance evidence for the selected approach.

Code examples and suggested direction

Current allocation path:

self.to_json()
    .map_err(serde::ser::Error::custom)?
    .serialize(serializer)

Suggested shape, using new private helpers that still need implementation:

// Check budgets AND JSON-compatible variants before the serializer writes.
preflight_json(self, &ParseLimits::default())
    .map_err(serde::ser::Error::custom)?;

// Borrow the validated tree instead of building an owned JSON copy.
// CheckedJson recursively serializes children without repeating preflight.
CheckedJson(self).serialize(serializer)

Checking budgets alone is insufficient: an unsupported nested native value must also be detected before output begins, matching the current conversion-before-serialization behavior.

Activity

  1. cblanquera commented on Oct 26, 2026

    @cblanquera
    MemberAuthor

    Suggested investigation and fix notes

    Deferred until trial/3 is merged into main. Recheck the merged source and tests first. The snippets are examples/proposals, not an approved patch or a change to frozen contracts.

    1. Separate validation from representation. Inspect Value::measure, json_at, Serialize for Value, Serialize for Nest, and Nest::path_as. Preserve default versus caller-selected limits at each entry point.
    2. Implement a private borrowed serializer wrapper after a complete JSON-compatibility/budget preflight. Use serialize_seq/serialize_map and borrow strings/keys. Recursive calls should serialize already-checked children instead of revalidating every subtree.
    3. Keep the current diagnostic paths, integer/number representation, insertion order, and unsupported-variant policy. Do not silently switch to externally tagged enum serialization through a derive.
    4. Treat typed deserialization as a separate, optional phase. A custom borrowed deserializer must retain DeserializeOwned behavior and equivalent type errors; leave it for a follow-up if that adds disproportionate complexity.
    5. Compare allocation counts or peak allocations for nested realistic data before/after. A second validation traversal may remain justified even when the owned intermediate tree is removed.

    Behavior example using current APIs:

    use stackpress_lib::{Result, Value};
    
    fn wire_example() -> Result<()> {
        let value = Value::try_from(serde_json::json!({
            "id": 7, "active": false, "optional": null
        }))?;
        assert_eq!(
            serde_json::to_string(&value)?,
            r#"{"id":7,"active":false,"optional":null}"#
        );
        assert!(serde_json::to_string(&Value::Bytes(vec![1, 2])).is_err());
        Ok(())
    }

    Extend data_contracts and request_response_contracts, including a custom serializer proving preflight failures do not begin output. Start with cargo test -p stackpress-lib --locked. Reference: implementing Serialize.

    Implementation workflow: After the merge, follow .agents/workflows/rust-programming.md: prove the affected behavior, complete the three test-verified review rounds, and record relevant formatting/lints/docs, feature/platform scope, and applicable development coverage. Preserve unrelated changes.

  2. cblanquera commented on Oct 26, 2026

    @cblanquera
    MemberAuthor

    Fixed on trial/3 in 644795f (merged as 64a5b70, task 00010 lane 1; not yet on main, so leaving open).

    • Value/Nest serialization and Nest::path_as now borrow through one private checked JSON view instead of building an owned serde_json::Value tree.
    • Budget validation and JSON-compatibility preflight both still run before any serializer/visitor call, with the original error precedence and path diagnostics (an empty-key path regression was caught and fixed during review).
    • Allocation evidence is recorded in the task receipt.

    Coordinator-verified: fmt, Clippy -D warnings, Rustdoc -D warnings, cargo +1.96.0 check, 454 workspace tests, 856 doctests pass; lib production coverage 93.64%.

  3. added a commit that references this issue on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions