Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ clap = { version = "4.6.1" }
insta = { version = "1.48.0" }
proc-macro2 = { version = "1.0.95" }
quote = { version = "1.0.40" }
serde = { version = "1.0.229", features = ["derive"] }
serde_derive_internals = { version = "0.29.1" }
serde_json = { version = "1.0.151" }
syn = { version = "2.0.104" }
which = { version = "8.0.4" }

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

`serde-shape` reflects the shape of Serde serialization and deserialization at compile time.

It gives libraries and tools a lightweight graph of the Rust types, Serde names, field metadata, enum tagging, defaults, aliases, skips, and custom serializer/deserializer boundaries that make up a type's wire shape.
It gives libraries and tools a lightweight graph of the Rust types, Serde names, field metadata, enum tagging, defaults, aliases, union value alternatives, skips, and custom serializer/deserializer boundaries that make up a type's wire shape.

## Install

Expand Down Expand Up @@ -48,7 +48,9 @@ Typical use cases:
- checking how a serialized or deserialized shape changes across releases;
- building schema exporters that start from Serde metadata.

`serde-shape` is intentionally not a full validation schema. It reflects the Serde data model shape and relevant Serde attributes; it does not infer value ranges, regexes, business rules, or runtime behavior hidden inside custom serializer/deserializer functions.
`serde-shape` is intentionally not a full validation schema. It reflects the Serde data model shape and relevant Serde attributes; it does not infer value ranges, regexes, business rules, or runtime behavior hidden inside custom serializer/deserializer functions. Use `ShapeRef::union` for format-native alternatives that do not fit one Rust shape. Union alternatives may overlap; they are flattened, deduplicated, and stored in canonical order.

Field shapes expose `wire_shape` as the source of truth for regular values, flattened fields, inline transparent fields, and omitted fields. Custom serializer/deserializer boundaries are represented by `ShapeRef::Opaque`, including when they are flattened or inline.

You may use [`schemars`](https://docs.rs/schemars) for JSON Schema generation and validation. But `schemars` is not a general-purpose Serde shape reflection library, and it does not support all Serde attributes. `serde-shape` is designed to be a more complete and general-purpose reflection of Serde shapes.

Expand Down
114 changes: 82 additions & 32 deletions serde-shape-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,22 +513,33 @@ fn serialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 {
let name = lit(variant.attrs.name().serialize_name());
let style = fields_style(variant.style);
let skip = variant.attrs.skip_serializing();
let custom_serializer = variant.attrs.serialize_with().is_some();
let untagged = variant.attrs.untagged();
let fields: Vec<_> = if skip || custom_serializer {
Vec::new()
let content = if skip {
quote!(::serde_shape::SerializeVariantContent::Omitted)
} else if let Some(custom_serializer) = variant.attrs.serialize_with() {
let detail = option_path(Some(custom_serializer));
quote! {
::serde_shape::SerializeVariantContent::Custom(::serde_shape::OpaqueShape {
type_name: ::core::any::type_name::<Self>(),
reason: ::serde_shape::OpaqueReason::CustomSerializer,
detail: #detail,
})
}
} else {
variant.fields.iter().map(serialize_field_shape).collect()
let fields = variant.fields.iter().map(serialize_field_shape);
quote! {
::serde_shape::SerializeVariantContent::Fields(
::serde_shape::__private::vec![#(#fields),*],
)
}
};

quote! {
::serde_shape::SerializeVariantShape {
rust_name: #rust_name,
name: #name,
style: #style,
fields: ::serde_shape::__private::vec![#(#fields),*],
skip: #skip,
custom_serializer: #custom_serializer,
content: #content,
untagged: #untagged,
}
}
Expand All @@ -540,13 +551,26 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 {
let aliases = aliases(variant.attrs.aliases());
let style = fields_style(variant.style);
let skip = variant.attrs.skip_deserializing();
let custom_deserializer = variant.attrs.deserialize_with().is_some();
let other = variant.attrs.other();
let untagged = variant.attrs.untagged();
let fields: Vec<_> = if skip || custom_deserializer {
Vec::new()
let content = if skip {
quote!(::serde_shape::DeserializeVariantContent::Omitted)
} else if let Some(custom_deserializer) = variant.attrs.deserialize_with() {
let detail = option_path(Some(custom_deserializer));
quote! {
::serde_shape::DeserializeVariantContent::Custom(::serde_shape::OpaqueShape {
type_name: ::core::any::type_name::<Self>(),
reason: ::serde_shape::OpaqueReason::CustomDeserializer,
detail: #detail,
})
}
} else {
variant.fields.iter().map(deserialize_field_shape).collect()
let fields = variant.fields.iter().map(deserialize_field_shape);
quote! {
::serde_shape::DeserializeVariantContent::Fields(
::serde_shape::__private::vec![#(#fields),*],
)
}
};

quote! {
Expand All @@ -555,9 +579,7 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 {
name: #name,
aliases: #aliases,
style: #style,
fields: ::serde_shape::__private::vec![#(#fields),*],
skip: #skip,
custom_deserializer: #custom_deserializer,
content: #content,
other: #other,
untagged: #untagged,
}
Expand All @@ -569,26 +591,40 @@ fn serialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 {
let name = lit(field.attrs.name().serialize_name());
let skip = field.attrs.skip_serializing();
let skip_if = option_path(field.attrs.skip_serializing_if());
let custom_serializer = field.attrs.serialize_with().is_some();
let flatten = field.attrs.flatten();
let transparent = field.attrs.transparent();
let ty = field.ty;
let value_shape = if skip || custom_serializer {
quote!(::core::option::Option::None)
let wire_shape = if skip {
quote!(::serde_shape::FieldWireShape::Omitted)
} else {
quote!(::core::option::Option::Some(<#ty as ::serde_shape::SerializeShape>::serialize_shape_in(context)))
let value_shape = if let Some(custom_serializer) = field.attrs.serialize_with() {
let detail = option_path(Some(custom_serializer));
quote! {
::serde_shape::ShapeRef::Opaque(::serde_shape::OpaqueShape {
type_name: ::core::any::type_name::<#ty>(),
reason: ::serde_shape::OpaqueReason::CustomSerializer,
detail: #detail,
})
}
} else {
quote!(<#ty as ::serde_shape::SerializeShape>::serialize_shape_in(context))
};

if transparent {
quote!(::serde_shape::FieldWireShape::Inline(#value_shape))
} else if flatten {
quote!(::serde_shape::FieldWireShape::Flatten(#value_shape))
} else {
quote!(::serde_shape::FieldWireShape::Value(#value_shape))
}
};

quote! {
::serde_shape::SerializeFieldShape {
member: #member,
name: #name,
value_shape: #value_shape,
flatten: #flatten,
skip: #skip,
wire_shape: #wire_shape,
skip_if: #skip_if,
custom_serializer: #custom_serializer,
transparent: #transparent,
}
}
}
Expand All @@ -598,28 +634,42 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 {
let name = lit(field.attrs.name().deserialize_name());
let aliases = aliases(field.attrs.aliases());
let skip = field.attrs.skip_deserializing();
let custom_deserializer = field.attrs.deserialize_with().is_some();
let default = default_shape(field.attrs.default());
let flatten = field.attrs.flatten();
let transparent = field.attrs.transparent();
let ty = field.ty;
let value_shape = if skip || custom_deserializer {
quote!(::core::option::Option::None)
let wire_shape = if skip {
quote!(::serde_shape::FieldWireShape::Omitted)
} else {
quote!(::core::option::Option::Some(<#ty as ::serde_shape::DeserializeShape>::deserialize_shape_in(context)))
let value_shape = if let Some(custom_deserializer) = field.attrs.deserialize_with() {
let detail = option_path(Some(custom_deserializer));
quote! {
::serde_shape::ShapeRef::Opaque(::serde_shape::OpaqueShape {
type_name: ::core::any::type_name::<#ty>(),
reason: ::serde_shape::OpaqueReason::CustomDeserializer,
detail: #detail,
})
}
} else {
quote!(<#ty as ::serde_shape::DeserializeShape>::deserialize_shape_in(context))
};

if transparent {
quote!(::serde_shape::FieldWireShape::Inline(#value_shape))
} else if flatten {
quote!(::serde_shape::FieldWireShape::Flatten(#value_shape))
} else {
quote!(::serde_shape::FieldWireShape::Value(#value_shape))
}
};

quote! {
::serde_shape::DeserializeFieldShape {
member: #member,
name: #name,
aliases: #aliases,
value_shape: #value_shape,
wire_shape: #wire_shape,
default: #default,
flatten: #flatten,
skip: #skip,
custom_deserializer: #custom_deserializer,
transparent: #transparent,
}
}
}
Expand Down
Loading