Skip to content
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.

### Breaking changes

* Represent Serde field and variant identifier enums with dedicated `Tagging` variants instead of incorrectly describing their input as externally tagged enums.
* Gate atomic shape implementations behind the `std` feature, matching Serde's own atomic implementations instead of advertising them in `no_std` builds where Serde cannot use them.
* Remove `DeserializeShape` from the unsized `str`, `[u8]`, and `Path` types, which do not implement Serde `Deserialize`. Their supported borrowed and owned forms retain explicit shapes.
* Replace the identical `SerializeTypeName` and `DeserializeTypeName` structures with one direction-neutral `TypeName`. Manual named definitions can use `TypeName::of::<T>(serde_name)` instead of repeating `core::any::type_name::<T>()`.
* Remove the redundant `transparent` field from container attributes. Transparent containers remain observable through their field's `FieldWireShape::Inline` position.
* Remove the blanket `DeserializeShape` implementations for `&T` and `&mut T`, which claimed support that Serde does not provide. Borrowed `&str`, `&[u8]`, and `&Path` inputs retain explicit implementations; custom borrowed types can now provide their own local implementation.
* Remove the redundant `tagging` and `has_flatten` fields from container attributes. Read enum tagging from `SerializeEnumShape::repr` or `DeserializeEnumShape::repr`, and identify flattened fields through `FieldWireShape::Flatten`.
Expand All @@ -17,6 +21,9 @@ All notable changes to this project will be documented in this file.

### New features

* Reflect serialized `core::fmt::Arguments` as a string, matching Serde's formatting implementation.
* Add target-specific `OsStr` and `OsString` enum shapes on Unix and Windows, including owned `Box<OsStr>` input.
* Add `SerializeShapeGraph::from_fn` and `DeserializeShapeGraph::from_fn` so custom shape functions can describe foreign graph roots without a dummy wrapper type.
* Add `#[serde_shape(serialize_with = "path", deserialize_with = "path")]` hooks for custom Serde functions and foreign representations.
* Allow custom shape hooks on enum variants so known custom variant content does not have to remain opaque.
* Reflect Serde's byte-buffer representation for `CStr`, `CString`, and owned `Box<CStr>` input.
Expand All @@ -32,7 +39,7 @@ All notable changes to this project will be documented in this file.
### Bug fixes

* Match Serde's deserialization bounds for tree and hash collections so a shape implementation is exposed only when the corresponding collection can actually deserialize.
* Preserve the known string and byte shapes of `#[serde(borrow)]` fields using `Cow<str>` or `Cow<[u8]>` instead of treating Serde's generated borrowing helpers as custom opaque deserializers.
* Preserve the known string and byte shapes of `#[serde(borrow)]` fields using `Cow<str>` or `Cow<[u8]>` from their source-level metadata, while leaving explicit custom deserializers opaque.
* Distinguish borrowed byte input from owned boxed slices: `&[u8]` reflects bytes while `Box<[u8]>` reflects a sequence, matching Serde.
* Match Serde's serialization bounds for `BinaryHeap`, `RefCell`, `Mutex`, and `RwLock`, including unsized wrapper contents.
* Reflect the proxy type used by Serde `from`, `try_from`, and `into` container attributes.
Expand All @@ -42,6 +49,7 @@ All notable changes to this project will be documented in this file.

### Improvements

* Add `definition_for` to both graph types so walkers can resolve a `ShapeRef::Definition` without repeating a match and id lookup.
* Verify the packaged main crate against the packaged derive implementation that will be released with it, rather than accidentally compiling the previously published same-version macro crate from crates.io.
* Clarify that shape graphs are normalized semantic models rather than exact traces of Serde serializer or deserializer method dispatch.
* Add `FieldWireShape::shape()` so graph walkers can follow any present field without repeating a match over every wire position.
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,25 @@ The built-in implementations follow Serde's semantic representations in each dir

| Group | Supported types |
| --- | --- |
| Scalars | Rust primitives, `String`, `str`, non-zero integers, and atomics available on the target |
| Scalars | Rust primitives, `String`, serialized `str` and `fmt::Arguments`, and non-zero integers |
| Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` |
| Wrappers | Serialized references, borrowed string/byte/path inputs, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Saturating`, `Reverse`, and `PhantomData` |
| FFI | `CStr` and `CString` byte representations, including owned `Box<CStr>` input |
| FFI | `CStr` and `CString` byte representations; on Unix and Windows, serialized `OsStr`, `OsString`, and owned `Box<OsStr>` input |
| Ranges | `Range`, `RangeFrom`, `RangeInclusive`, `RangeTo`, and `Bound` |
| Time | `core::time::Duration` and, with `std`, `SystemTime` |
| Network | `core::net` IP and socket address types |
| `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` |
| `std` feature | Atomics available on the target, `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` |

Network address shapes are unions of their human-readable string representation and their compact Serde representation. A serialized byte slice and an owned `Box<[u8]>` input are sequences, while borrowed byte deserialization uses `ShapeRef::Bytes`.

OS string shapes preserve Serde's target-specific externally tagged representation: `Unix` contains a byte sequence, while `Windows` contains a `u16` sequence. Deserialization advertises only the variant accepted on the current target.

Serde's `rc` feature is still required to serialize or deserialize `Rc`, `Arc`, and their weak pointers; the shape implementations do not enable Serde features.

Serialization follows Serde's blanket support for `&T` and `&mut T`. Deserialization only provides reference shapes for Serde's borrowable `&str`, `&[u8]`, and `&Path` inputs; arbitrary shared and mutable references do not have a Serde deserializer.

The unsized `str`, `[u8]`, and `Path` types themselves do not implement `DeserializeShape`, matching Serde. Their borrowed and owned input forms have explicit shape implementations.

For an unsupported foreign type, use a local newtype and implement `SerializeShape` or `DeserializeShape` manually. Custom Serde functions remain opaque by default because their wire behavior cannot be inferred; use a `serde_shape` custom hook when the representation is known.

## `no_std` support
Expand Down
133 changes: 100 additions & 33 deletions serde-shape-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use serde_derive_internals::Derive;
use serde_derive_internals::ast;
use serde_derive_internals::attr;
use serde_derive_internals::name::Name;
use serde_derive_internals::ungroup;
use syn::DeriveInput;
use syn::GenericArgument;
use syn::LitStr;
Expand Down Expand Up @@ -144,6 +145,18 @@ fn parse_container<'a>(input: &'a DeriveInput, derive: Derive) -> syn::Result<as
));
};
cx.check()?;

if matches!(derive, Derive::Serialize) {
let message = match container.attrs.identifier() {
attr::Identifier::No => None,
attr::Identifier::Field => Some("field identifiers cannot be serialized"),
attr::Identifier::Variant => Some("variant identifiers cannot be serialized"),
};
if let Some(message) = message {
return Err(syn::Error::new_spanned(input, message));
}
}

Ok(container)
}

Expand Down Expand Up @@ -535,10 +548,7 @@ fn serialize_shape_body(

Ok(quote! {
context.define_named_type_with_description(
__serde_shape::SerializeTypeName {
rust_name: ::core::any::type_name::<Self>(),
name: #name,
},
__serde_shape::TypeName::of::<Self>(#name),
#description,
|context| {
#kind
Expand Down Expand Up @@ -569,10 +579,7 @@ fn deserialize_shape_body(

Ok(quote! {
context.define_named_type_with_description(
__serde_shape::DeserializeTypeName {
rust_name: ::core::any::type_name::<Self>(),
name: #name,
},
__serde_shape::TypeName::of::<Self>(#name),
#description,
|context| {
#kind
Expand Down Expand Up @@ -643,7 +650,7 @@ fn deserialize_definition_kind(container: &ast::Container<'_>) -> syn::Result<To
}
}
ast::Data::Enum(variants) => {
let repr = tagging(container.attrs.tag());
let repr = deserialize_tagging(&container.attrs);
let variants = variants
.iter()
.map(deserialize_variant_shape)
Expand Down Expand Up @@ -849,6 +856,7 @@ fn serialize_field_shape(field: &ast::Field<'_>) -> syn::Result<TokenStream2> {

fn deserialize_field_shape(field: &ast::Field<'_>) -> syn::Result<TokenStream2> {
let shape_attrs = ShapeAttrs::parse(&field.original.attrs)?;
let borrowed_cow_shape = borrowed_cow_shape(field)?;
let member = field_member(&field.member);
let name = lit_name(field.attrs.name().deserialize_name());
let aliases = aliases(field.attrs.aliases());
Expand All @@ -864,18 +872,16 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> syn::Result<TokenStream2>
} else {
let value_shape = if let Some(function) = shape_attrs.deserialize_with() {
quote!(#function(context))
} else if let Some(shape) = borrowed_cow_shape {
shape
} else if let Some(custom_deserializer) = field.attrs.deserialize_with() {
if let Some(shape) = serde_borrowed_cow_shape(custom_deserializer) {
shape
} else {
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,
})
}
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))
Expand Down Expand Up @@ -943,6 +949,14 @@ fn tagging(tag: &attr::TagType) -> TokenStream2 {
}
}

fn deserialize_tagging(attrs: &attr::Container) -> TokenStream2 {
match attrs.identifier() {
attr::Identifier::No => tagging(attrs.tag()),
attr::Identifier::Field => quote!(__serde_shape::Tagging::FieldIdentifier),
attr::Identifier::Variant => quote!(__serde_shape::Tagging::VariantIdentifier),
}
}

fn default_shape(default: &attr::Default) -> TokenStream2 {
match default {
attr::Default::None => quote!(__serde_shape::DefaultShape::None),
Expand All @@ -959,27 +973,80 @@ fn aliases(aliases: &BTreeSet<Name>) -> TokenStream2 {
quote!(__serde_shape::__private::vec![#(#aliases),*])
}

fn serde_borrowed_cow_shape(path: &syn::ExprPath) -> Option<TokenStream2> {
if path.qself.is_some() || path.path.leading_colon.is_some() {
return None;
fn borrowed_cow_shape(field: &ast::Field<'_>) -> syn::Result<Option<TokenStream2>> {
// serde_derive_internals models borrowed Cow fields as custom deserializers internally. Read
// the source-level contract instead, so this derive does not depend on Serde's private helper
// path. An explicit user deserializer still takes precedence over the built-in Cow behavior.
if field.attrs.borrowed_lifetimes().is_empty()
|| has_explicit_serde_deserializer(&field.original.attrs)?
{
return Ok(None);
}

let mut segments = path.path.segments.iter();
let serde = segments.next()?;
let _private = segments.next()?;
let de = segments.next()?;
let helper = segments.next()?;
if segments.next().is_some() || serde.ident != "_serde" || de.ident != "de" {
return None;
let Some(element) = cow_element_type(field.ty) else {
return Ok(None);
};
if is_primitive_type(element, "str") {
Ok(Some(quote!(__serde_shape::ShapeRef::String)))
} else if is_byte_slice(element) {
Ok(Some(quote!(__serde_shape::ShapeRef::Bytes)))
} else {
Ok(None)
}
}

fn has_explicit_serde_deserializer(attrs: &[syn::Attribute]) -> syn::Result<bool> {
for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) {
let metas = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
)?;
if metas
.iter()
.any(|meta| meta.path().is_ident("deserialize_with") || meta.path().is_ident("with"))
{
return Ok(true);
}
}
Ok(false)
}

match helper.ident.to_string().as_str() {
"borrow_cow_str" => Some(quote!(__serde_shape::ShapeRef::String)),
"borrow_cow_bytes" => Some(quote!(__serde_shape::ShapeRef::Bytes)),
fn cow_element_type(ty: &Type) -> Option<&Type> {
let Type::Path(ty) = ungroup(ty) else {
return None;
};
let segment = ty.path.segments.last()?;
let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return None;
};
let mut arguments = arguments.args.iter();
match (arguments.next(), arguments.next(), arguments.next()) {
(Some(GenericArgument::Lifetime(_)), Some(GenericArgument::Type(element)), None)
if segment.ident == "Cow" =>
{
Some(element)
}
_ => None,
}
}

fn is_byte_slice(ty: &Type) -> bool {
match ungroup(ty) {
Type::Slice(slice) => is_primitive_type(&slice.elem, "u8"),
_ => false,
}
}

fn is_primitive_type(ty: &Type, name: &str) -> bool {
let Type::Path(ty) = ungroup(ty) else {
return false;
};
ty.qself.is_none()
&& ty.path.leading_colon.is_none()
&& ty.path.segments.len() == 1
&& ty.path.segments[0].ident == name
&& ty.path.segments[0].arguments.is_empty()
}

fn lit_name(value: &Name) -> LitStr {
LitStr::new(&value.value, value.span)
}
Expand Down
66 changes: 23 additions & 43 deletions serde-shape/src/impls/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// limitations under the License.

use alloc::vec;
use core::any::type_name;
use core::ops::Bound;

use crate::DefaultShape;
Expand All @@ -23,7 +22,6 @@ use crate::DeserializeEnumShape;
use crate::DeserializeFieldShape;
use crate::DeserializeShape;
use crate::DeserializeShapeContext;
use crate::DeserializeTypeName;
use crate::DeserializeVariantContent;
use crate::DeserializeVariantShape;
use crate::FieldMember;
Expand All @@ -35,34 +33,28 @@ use crate::SerializeEnumShape;
use crate::SerializeFieldShape;
use crate::SerializeShape;
use crate::SerializeShapeContext;
use crate::SerializeTypeName;
use crate::SerializeVariantContent;
use crate::SerializeVariantShape;
use crate::ShapeRef;
use crate::Tagging;
use crate::TypeName;

impl<T> SerializeShape for Bound<T>
where
T: SerializeShape,
{
fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef {
context.define_named_type(
SerializeTypeName {
rust_name: type_name::<Self>(),
name: "Bound",
},
|context| {
SerializeDefinitionKind::Enum(SerializeEnumShape {
repr: Tagging::External,
variants: vec![
serialize_bound_variant("Unbounded", None),
serialize_bound_variant("Included", Some(T::serialize_shape_in(context))),
serialize_bound_variant("Excluded", Some(T::serialize_shape_in(context))),
],
attributes: SerializeContainerAttributes::default(),
})
},
)
context.define_named_type(TypeName::of::<Self>("Bound"), |context| {
SerializeDefinitionKind::Enum(SerializeEnumShape {
repr: Tagging::External,
variants: vec![
serialize_bound_variant("Unbounded", None),
serialize_bound_variant("Included", Some(T::serialize_shape_in(context))),
serialize_bound_variant("Excluded", Some(T::serialize_shape_in(context))),
],
attributes: SerializeContainerAttributes::default(),
})
})
}
}

Expand All @@ -71,29 +63,17 @@ where
T: DeserializeShape,
{
fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef {
context.define_named_type(
DeserializeTypeName {
rust_name: type_name::<Self>(),
name: "Bound",
},
|context| {
DeserializeDefinitionKind::Enum(DeserializeEnumShape {
repr: Tagging::External,
variants: vec![
deserialize_bound_variant("Unbounded", None),
deserialize_bound_variant(
"Included",
Some(T::deserialize_shape_in(context)),
),
deserialize_bound_variant(
"Excluded",
Some(T::deserialize_shape_in(context)),
),
],
attributes: DeserializeContainerAttributes::default(),
})
},
)
context.define_named_type(TypeName::of::<Self>("Bound"), |context| {
DeserializeDefinitionKind::Enum(DeserializeEnumShape {
repr: Tagging::External,
variants: vec![
deserialize_bound_variant("Unbounded", None),
deserialize_bound_variant("Included", Some(T::deserialize_shape_in(context))),
deserialize_bound_variant("Excluded", Some(T::deserialize_shape_in(context))),
],
attributes: DeserializeContainerAttributes::default(),
})
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions serde-shape/src/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ mod bound;
mod container;
mod ffi;
mod net;
#[cfg(all(feature = "std", any(unix, windows)))]
mod os_string;
mod primitive;
mod range;
mod result;
Expand Down
Loading