Skip to main content

Crate serde_shape

Crate serde_shape 

Source
Expand description

§serde-shape

serde-shape reflects the data model that a Rust type emits through Serde serialization or accepts through Serde deserialization. It builds a lightweight graph from type information and #[serde(...)] attributes without serializing or deserializing a value.

Common uses are generating configuration reference docs, deriving environment-variable maps from config structs, documenting wire formats, and checking whether two versions of a type expose compatible Serde shapes.

§Getting started

Enable the derive feature when you want #[derive(SerializeShape)] and #[derive(DeserializeShape)]:

[dependencies]
serde-shape = { version = "0.1.0", features = ["derive"] }

Enable std when the reflected types use shapes provided only by the Rust standard library:

[dependencies]
serde-shape = { version = "0.1.0", features = ["derive", "std"] }

The crate is no_std by default and requires alloc. The shape derives are independent of Serde’s Serialize and Deserialize derives: they neither implement nor require those traits. Derive both sets when a type must also perform actual serialization or deserialization. serde_shape attributes affect reflection metadata only; they do not change Serde’s runtime behavior.

§Inspecting a graph

Derive DeserializeShape for the type you want to inspect, then build a DeserializeShapeGraph:

use serde_shape::DeserializeDefinitionKind;
use serde_shape::DeserializeShape;
use serde_shape::FieldsStyle;

#[derive(DeserializeShape)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Config {
    http_port: u16,
    peers: Vec<String>,
    tls: Option<TlsConfig>,
}

#[derive(DeserializeShape)]
#[serde(rename_all = "kebab-case")]
struct TlsConfig {
    cert_path: String,
    key_path: String,
}

let graph = Config::deserialize_shape();
let config = graph.root_definition().unwrap();

let DeserializeDefinitionKind::Struct(shape) = &config.kind else {
    panic!("Config should produce a struct shape");
};

assert_eq!(config.type_name.name, "Config");
assert_eq!(shape.style, FieldsStyle::Struct);
assert!(shape.attributes.deny_unknown_fields);
assert_eq!(shape.fields[0].name, "http-port");
assert_eq!(shape.fields[1].name, "peers");
assert_eq!(shape.fields[2].name, "tls");

Serialization and deserialization are reflected separately because Serde lets the two directions differ:

use serde_shape::DeserializeDefinitionKind;
use serde_shape::DeserializeShape;
use serde_shape::SerializeDefinitionKind;
use serde_shape::SerializeShape;

#[derive(SerializeShape, DeserializeShape)]
#[serde(rename(serialize = "wire-output", deserialize = "wire-input"))]
struct Message {
    #[serde(rename(serialize = "out-id", deserialize = "in-id"))]
    id: u64,
}

let serialize_graph = Message::serialize_shape();
let deserialize_graph = Message::deserialize_shape();
let serialize_definition = serialize_graph.root_definition().unwrap();
let deserialize_definition = deserialize_graph.root_definition().unwrap();

assert_eq!(serialize_definition.type_name.name, "wire-output");
assert_eq!(deserialize_definition.type_name.name, "wire-input");

let SerializeDefinitionKind::Struct(serialize_shape) = &serialize_definition.kind else {
    panic!("Message should produce a struct serialization shape");
};
let DeserializeDefinitionKind::Struct(deserialize_shape) = &deserialize_definition.kind else {
    panic!("Message should produce a struct deserialization shape");
};

assert_eq!(serialize_shape.fields[0].name, "out-id");
assert_eq!(deserialize_shape.fields[0].name, "in-id");

§Shape graphs

A shape graph has a ShapeRef root and a list of named definitions. Flat primitive and compound values are represented directly as ShapeRef values. Structs and enums are stored as named definitions and referenced by ShapeId.

Definition IDs are local to one graph. They contain an index but no graph identity, so callers must keep each ID paired with the graph that produced it. Use SerializeShapeGraph::definition or DeserializeShapeGraph::definition to resolve them. Definition ordering and debug output are not stable persistence formats. Definitions may be recursive, so graph walkers must detect repeated ShapeId values before following definition references.

Types that branch on Serde’s human-readable mode may expose a union of their known representations. Shape graphs describe possible semantic shapes across formats rather than specializing themselves for one serializer.

ShapeRef is not a trace of exact serializer or deserializer method calls. It deliberately preserves useful Rust distinctions such as fixed arrays and pointer-width integers even when Serde dispatches them through tuple or fixed-width integer methods.

§Derive behavior

The derive macros use Serde’s derive metadata for the selected direction. They reflect directional names and skips, rename rules, aliases, defaults, enum tagging, flattening, transparent fields, identifier enums, conversion types, remote definitions, and custom serializer or deserializer boundaries.

A custom serializer or deserializer has no inferable inner shape, so the affected field or variant content is represented by an opaque boundary. Whole-container conversion attributes use the conversion type’s shape. Serde remote derives expose the helper definition’s declared wire shape. Field-level FieldWireShape distinguishes ordinary values from flattened fields, inline transparent fields, and omitted fields. Custom serializer/deserializer boundaries use ShapeRef::Opaque and remain composable with those field positions.

Use #[serde_shape(serialize_with = "path")] or #[serde_shape(deserialize_with = "path")] to declare the representation of a container, variant, or field that cannot be inferred. Each function receives the current graph context and returns a ShapeRef, so it can delegate to another type or build a custom shape directly. These extensions supply reflection metadata only: they cannot rename, tag, skip, flatten, alias, or default a Serde item.

Rust doc comments on derived containers, variants, and fields are preserved in their description fields for documentation and diagnostic consumers.

§Manual implementations

Implement SerializeShape or DeserializeShape manually when a type’s Serde representation is known but cannot be derived. This is common for wrappers that deserialize from a string or another primitive representation:

use serde_shape::DeserializeShape;
use serde_shape::DeserializeShapeContext;
use serde_shape::ShapeRef;

struct ByteSize(u64);

impl DeserializeShape for ByteSize {
    fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef {
        ShapeRef::union([ShapeRef::String, ShapeRef::U64])
    }
}

assert_eq!(
    ByteSize::deserialize_shape().root(),
    &ShapeRef::union([ShapeRef::String, ShapeRef::U64])
);

For recursive or shared named types, use SerializeShapeContext::define_named_type or DeserializeShapeContext::define_named_type so the graph contains one definition and all recursive edges point back to it.

Structs§

DeserializeContainerAttributes
Container metadata relevant to deserialization.
DeserializeDefinitionShape
A named type definition in a deserialization shape graph.
DeserializeEnumShape
Enum-like deserialization metadata.
DeserializeFieldShape
Field-level deserialization metadata.
DeserializeShapeContext
A context that accumulates named deserialization definitions while a graph is built.
DeserializeShapeGraph
A complete deserialization shape graph rooted at one type.
DeserializeStructShape
Struct-like deserialization metadata.
DeserializeVariantShape
Variant-level deserialization metadata.
OpaqueShape
An intentionally opaque shape.
SerializeContainerAttributes
Container metadata relevant to serialization.
SerializeDefinitionShape
A named type definition in a serialization shape graph.
SerializeEnumShape
Enum-like serialization metadata.
SerializeFieldShape
Field-level serialization metadata.
SerializeShapeContext
A context that accumulates named serialization definitions while a graph is built.
SerializeShapeGraph
A complete serialization shape graph rooted at one type.
SerializeStructShape
Struct-like serialization metadata.
SerializeVariantShape
Variant-level serialization metadata.
ShapeId
A graph-local identifier for a named shape definition.
TypeName
Names associated with a Rust type and one direction of its Serde representation.
UnionShape
The normalized alternatives contained by ShapeRef::Union.

Enums§

DefaultShape
A Serde default marker.
DeserializeDefinitionKind
The body of a named deserialization definition.
DeserializeVariantContent
The deserialized content controlled by an enum variant.
FieldMember
The Rust member represented by a field.
FieldWireShape
How a field contributes to the wire representation in one Serde direction.
FieldsStyle
The style of a struct, variant, or tuple field list.
OpaqueReason
The reason a shape cannot be represented precisely.
SerializeDefinitionKind
The body of a named serialization definition.
SerializeVariantContent
The serialized content controlled by an enum variant.
ShapeRef
A reference to a shape node.
Tagging
Serde container or enum tagging representation.

Traits§

DeserializeShape
A type that can describe the shape accepted by its Serde deserializer.
SerializeShape
A type that can describe the shape emitted by its Serde serializer.

Derive Macros§

DeserializeShapederive
Derives DeserializeShape from Serde deserialization metadata.
SerializeShapederive
Derives SerializeShape from Serde serialization metadata.