Skip to content

Add .builder_shape field to Shape for immutable collections with mutable builders #1290

@fasterthanlime

Description

@fasterthanlime

Problem

Currently, .inner on Shape is overloaded for multiple purposes:

  1. Variance computation: Vec<T>, Map<K,V>, etc. set .inner(T::SHAPE) to avoid monomorphizing a variance function per generic instantiation
  2. Transparent wrappers: Types that deserialize as their inner type (e.g., newtypes)
  3. Immutable collections with builders: Bytes and Arc<[T]> use .inner to specify they build through BytesMut and Vec<T> respectively

This conflation causes problems for deserializers. After commit d7479d1 added .inner(T::SHAPE) to Vec for variance, deserializers broke because they couldn't distinguish:

  • "I'm Vec<T> with .inner for variance only - deserialize me as a List"
  • "I'm Bytes with .inner for building - deserialize me as BytesMut then convert"

Current Workarounds

Deserializers check Def::List and look for init_in_place_with_capacity() to distinguish:

  • Lists WITH init_in_place_with_capacity → use List deserializer (Vec)
  • Lists WITHOUT init_in_place_with_capacity → use .inner path (Bytes)

This is fragile and encodes too much type-specific knowledge in deserializers.

Proposed Solution

Add a new .builder_shape field to Shape:

pub struct Shape {
    // ... existing fields ...
    
    /// Optional builder type for immutable collections.
    /// If set, deserializers should build this type first, then convert to the target type.
    pub builder_shape: Option<&'static Shape>,
}

Usage

Bytes:

.builder_shape(Some(BytesMut::SHAPE))
.inner(u8::SHAPE)  // Still set for "element type" semantics if needed

Arc<[T]>:

.builder_shape(Some(Vec::<T>::SHAPE))
.inner(T::SHAPE)  // For variance

Vec:

.builder_shape(None)  // Builds directly
.inner(T::SHAPE)  // For variance

Deserializer Logic

// 1. Check builder_shape first
if let Some(builder) = shape.builder_shape {
    partial = deserialize_into(partial, builder)?;
    partial = convert_from_builder(partial)?;  // Uses TryFrom or vtable
    return Ok(partial);
}

// 2. Check Def for fundamental collection types
match &shape.def {
    Def::List(_) => deserialize_list(partial)?,
    Def::Map(_) => deserialize_map(partial)?,
    Def::Set(_) => deserialize_set(partial)?,
    Def::Array(_) => deserialize_array(partial)?,
    // ...
}

// 3. Check .inner for transparent wrappers
if let Some(inner) = shape.inner {
    partial = partial.begin_inner()?;
    partial = deserialize_into(partial, inner)?;
    partial = partial.end()?;
    return Ok(partial);
}

// 4. Check Type (Struct, Enum, etc.)
// ...

Benefits

  1. Clearer separation of concerns:

    • .builder_shape: "build me through this type"
    • .inner: "for variance or transparent wrapping"
    • Def: "what I am semantically"
  2. Generic solution: Works for any immutable collection, not just Lists

  3. No type-specific knowledge in deserializers: No checking for init_in_place_with_capacity presence

  4. Explicit intent: Code clearly shows "this builds through a builder"

Implementation Scope

This affects:

  • facet-core: Add .builder_shape field to Shape and ShapeBuilder
  • facet-json, facet-xml, facet-value, etc.: Update deserializer routing logic
  • Bytes, Arc<[T]>: Set .builder_shape instead of relying on .inner for building
  • Vec, Map, Set, Array: Keep .inner for variance only
  • facet-reflect: Add Partial support for builder conversion

Related

  • Commit d7479d1: Added .inner to collections for variance, exposing this issue
  • Current workaround in deserializers checks init_in_place_with_capacity() presence

Metadata

Metadata

Assignees

No one assigned

    Labels

    ⚙️ corefacet-core crate, core types and traits✨ enhancementNew feature or request📄 xmlfacet-xml crate, XML serialization/deserialization🔵 jsonfacet-json crate, JSON serialization/deserialization

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions