mod aliases;
mod attributes;
mod shape;
use aliases::shadowed_variant;
use attributes::{
constraints, described_members, field_name, is_described, is_flattened, is_open, is_option,
is_phantom, is_required, is_skipped_both_ways, is_unit_like, open_span, serde_flag,
serde_key_span, transparent_member, transparent_picks, variant_name,
};
use shape::{enum_body, struct_body};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use syn::{
Data, DataEnum, DeriveInput, Field, Fields, Lit, LitFloat, LitInt, LitStr, Type, Variant,
ext::IdentExt, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use crate::derive::common::{doc_string, is_deprecated, skip_value};
const NUMERIC: &[&str] = &[
"minimum",
"maximum",
"exclusive_minimum",
"exclusive_maximum",
"multiple_of",
];
const COUNTS: &[&str] = &["min_length", "max_length", "min_items", "max_items"];
const FLAGS: &[&str] = &["unique_items", "open"];
const WIRE_FORM_OVERRIDES: &[&str] = &["with", "serialize_with", "deserialize_with"];
const READ_OVERRIDES: &[&str] = &["with", "deserialize_with"];
const WRITE_OVERRIDES: &[&str] = &["with", "serialize_with"];
const CONVERSIONS: &[&str] = &["into", "from", "try_from"];
pub(crate) fn expand(item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
match expand_inner(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
pub(super) fn expand_inner(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
if let Data::Union(data) = &input.data {
return Err(syn::Error::new(
data.union_token.span(),
"`Schema` cannot describe a union: no JSON value corresponds to one",
));
}
reject_container_conversions(input)?;
reject_untagged(input)?;
reject_unread_variant(input)?;
reject_shadowed_variant(input)?;
reject_wire_form_overrides(input)?;
reject_catch_all(input)?;
reject_transparent_without_one_field(input)?;
reject_read_required_skip(input)?;
reject_contradicted_closure(input)?;
reject_closed_tagged_struct(input)?;
reject_field_named_as_tag(input)?;
reject_unread_field_in_closed_object(input)?;
reject_one_way_member_skip(input)?;
reject_skipped_adjacent_payload(input)?;
check_constraints(input)?;
let name = &input.ident;
let generics = schema_bounded_generics(input);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let component = LitStr::new(&name.to_string(), name.span());
let named = if input.generics.type_params().next().is_some() {
quote!(::core::option::Option::None)
} else {
quote!(::kynos::openapi::ComponentName::sanitized(#component).ok())
};
let container = Container::read(input);
let body = body(input, &container);
let witnesses = flatten_witnesses(input, &container, &generics);
let flatten = flattens(input, &container).then(|| {
quote! {
#[allow(deprecated)]
impl #impl_generics ::kynos::schema::flatten::Flatten
for #name #ty_generics #where_clause {}
}
});
let closed_flatten = flattens_into_closed_objects(input, &container).then(|| {
quote! {
#[allow(deprecated)]
impl #impl_generics ::kynos::schema::flatten::ClosedFlatten
for #name #ty_generics #where_clause {}
}
});
Ok(quote! {
#witnesses
#flatten
#closed_flatten
#[allow(deprecated)]
impl #impl_generics ::kynos::schema::Schema for #name #ty_generics #where_clause {
fn schema(
registry: &mut ::kynos::schema::registry::Registry,
) -> ::kynos::openapi::Schema {
#body
}
fn name() -> ::core::option::Option<::kynos::openapi::ComponentName> {
#named
}
}
})
}
fn schema_bounded_generics(input: &DeriveInput) -> syn::Generics {
let mut generics = input.generics.clone();
let parameters: Vec<syn::Ident> = generics
.type_params()
.map(|parameter| parameter.ident.clone())
.collect();
if parameters.is_empty() {
return generics;
}
let clause = generics.make_where_clause();
for parameter in parameters {
clause
.predicates
.push(syn::parse_quote!(#parameter: ::kynos::schema::Schema));
}
generics
}
fn flatten_witnesses(
input: &DeriveInput,
container: &Container,
generics: &syn::Generics,
) -> TokenStream2 {
let (impl_generics, _, where_clause) = generics.split_for_impl();
let closing = container.deny_unknown_fields && !container.transparent;
let flattened = described_groups(input)
.into_iter()
.flat_map(described_members)
.filter(|field| is_flattened(field))
.map(|field| (field, closing));
let payloads: Vec<&Field> = match (&input.data, &container.tag, &container.content) {
(Data::Enum(data), Some(_), None) => described_variants(data)
.into_iter()
.filter(|variant| !is_unit_like(&variant.fields))
.filter_map(|variant| match &variant.fields {
Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => unnamed.unnamed.first(),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let admitting = open_fields_beside_unread_fields(input, container);
let payloads = payloads.into_iter().map(|field| (field, false));
let witnesses = flattened.chain(payloads).map(|(field, closed)| {
let ty = &field.ty;
if admitting.iter().any(|open| std::ptr::eq(*open, field)) {
quote_spanned! {ty.span()=>
const _: () = {
#[allow(dead_code, deprecated)]
fn open_fields_beside_unread_fields_admit_any_member #impl_generics ()
#where_clause
{
fn admits_any<T: ::kynos::schema::flatten::AdmitsAny + ?Sized>() {}
admits_any::<#ty>();
}
};
}
} else if is_open(field) {
quote_spanned! {ty.span()=>
const _: () = {
#[allow(dead_code, deprecated)]
fn open_fields_are_maps_described_in_place #impl_generics () #where_clause {
fn is_open_map<T: ::kynos::schema::flatten::OpenMap + ?Sized>() {}
is_open_map::<#ty>();
}
};
}
} else {
let read_by_name = closed.then(|| {
quote_spanned! {ty.span()=>
const _: () = {
#[allow(dead_code, deprecated)]
fn closed_objects_flatten_what_serde_reads_by_name #impl_generics ()
#where_clause
{
fn is_closed_flattenable<
T: ::kynos::schema::flatten::ClosedFlatten + ?Sized,
>() {}
is_closed_flattenable::<#ty>();
}
};
}
});
quote_spanned! {ty.span()=>
const _: () = {
#[allow(dead_code, deprecated)]
fn flattened_fields_name_their_members #impl_generics () #where_clause {
fn is_flattenable<T: ::kynos::schema::flatten::Flatten + ?Sized>() {}
is_flattenable::<#ty>();
}
};
#read_by_name
}
}
});
quote!(#(#witnesses)*)
}
fn open_fields_beside_unread_fields<'a>(
input: &'a DeriveInput,
container: &Container,
) -> Vec<&'a Field> {
if container.transparent {
return Vec::new();
}
written_groups(input)
.into_iter()
.filter(|fields| unread_field_span(fields).is_some())
.flat_map(described_members)
.filter(|field| is_open(field))
.collect()
}
fn written_groups(input: &DeriveInput) -> Vec<&Fields> {
match &input.data {
Data::Struct(data) => vec![&data.fields],
Data::Enum(data) => data
.variants
.iter()
.filter(|variant| is_written(variant))
.map(|variant| &variant.fields)
.collect(),
Data::Union(_) => Vec::new(),
}
}
fn flattens(input: &DeriveInput, container: &Container) -> bool {
if container.transparent {
return false;
}
let groups = described_groups(input);
if groups.iter().flat_map(|group| group.iter()).any(is_open) {
return false;
}
match &input.data {
Data::Struct(data) => {
matches!(data.fields, Fields::Named(_))
&& unread_field_span(&data.fields).is_none()
&& !container.deny_unknown_fields
}
Data::Enum(data) => {
let variants = described_variants(data);
match (&container.tag, &container.content) {
(Some(_), Some(_)) => !variants.is_empty() && !container.deny_unknown_fields,
(Some(_), None) => {
!variants.is_empty()
&& variants.iter().all(|variant| match variant.fields {
Fields::Unit => true,
Fields::Named(_) => !container.deny_unknown_fields,
Fields::Unnamed(_) => false,
})
&& variants
.iter()
.filter(|variant| is_written(variant))
.all(|variant| unread_field_span(&variant.fields).is_none())
}
(None, _) => false,
}
}
Data::Union(_) => false,
}
}
fn flattens_into_closed_objects(input: &DeriveInput, container: &Container) -> bool {
if !flattens(input, container) {
return false;
}
match &input.data {
Data::Struct(data) => {
container.tag.is_none()
&& !data.fields.iter().any(|field| {
is_flattened(field)
&& !serde_flag(&field.attrs, &["skip", "skip_deserializing"])
})
}
Data::Enum(_) => container.content.is_some(),
Data::Union(_) => false,
}
}
fn check_constraints(input: &DeriveInput) -> syn::Result<()> {
for group in field_groups(input) {
let named = match group {
Fields::Named(named) => &named.named,
Fields::Unnamed(unnamed) => &unnamed.unnamed,
Fields::Unit => continue,
};
let mut opened: Option<Span> = None;
for field in named {
for attr in &field.attrs {
if attr.path().is_ident("schema") {
attr.parse_nested_meta(|meta| check_constraint(&meta))?;
}
}
let Some(span) = open_span(field) else {
continue;
};
if !is_flattened(field) {
return Err(syn::Error::new(
span,
"`#[schema(open)]` says what a flattened field contributes to the object \
carrying it, and only a flattened field has anything to contribute: an \
ordinary field is one property, whose own schema already states what it \
admits. Add `#[serde(flatten)]`, or drop the attribute",
));
}
if opened.is_some() {
return Err(syn::Error::new(
span,
"`#[schema(open)]` may appear once per container object: it supplies that \
object's `unevaluatedProperties`, which is one keyword, so a second open \
field could only overwrite what the first one said. Merge the two maps, or \
give one of them a named field of its own",
));
}
opened = Some(span);
}
}
Ok(())
}
fn field_groups(input: &DeriveInput) -> Vec<&Fields> {
match &input.data {
Data::Struct(data) => vec![&data.fields],
Data::Enum(data) => data
.variants
.iter()
.map(|variant| &variant.fields)
.collect(),
Data::Union(_) => Vec::new(),
}
}
fn described_groups(input: &DeriveInput) -> Vec<&Fields> {
match &input.data {
Data::Enum(data) => described_variants(data)
.into_iter()
.map(|variant| &variant.fields)
.collect(),
_ => field_groups(input),
}
}
fn described_variants(data: &DataEnum) -> Vec<&Variant> {
data.variants
.iter()
.filter(|variant| !is_skipped_both_ways(&variant.attrs))
.collect()
}
fn is_written(variant: &Variant) -> bool {
!serde_flag(&variant.attrs, &["skip", "skip_serializing"])
}
fn check_constraint(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
let Some(key) = meta.path.get_ident() else {
return Ok(());
};
let name = key.to_string();
if name == "format" {
return Err(syn::Error::new(
key.span(),
"`format` says what a value *is*, which follows from its type rather than from the \
field carrying it. Use a type that already claims the format -- `uuid::Uuid` behind \
the `uuid` feature, a date or time type behind `time-chrono` or `time-jiff`, a \
decimal behind `decimal-rust` or `decimal-big` -- or give the value a newtype with \
its own `Schema` implementation. `pattern` is here if what you meant is a \
constraint on this field rather than a claim about the type",
));
}
if FLAGS.contains(&name.as_str()) {
return if meta.input.peek(syn::Token![=]) {
Err(syn::Error::new(
key.span(),
format!("`{name}` is a flag; write it alone, or leave it out"),
))
} else {
Ok(())
};
}
if NUMERIC.contains(&name.as_str()) {
return match meta.value()?.parse()? {
Lit::Int(_) | Lit::Float(_) => Ok(()),
other => Err(syn::Error::new(
other.span(),
format!("`{name}` takes a number"),
)),
};
}
if COUNTS.contains(&name.as_str()) {
let literal = meta.value()?.parse()?;
return match &literal {
Lit::Int(value) => value.base10_parse::<u64>().map(|_| ()),
other => Err(syn::Error::new(
other.span(),
format!("`{name}` takes a non-negative whole number"),
)),
};
}
if name == "pattern" {
return meta.value()?.parse::<LitStr>().map(|_| ());
}
Err(syn::Error::new(
key.span(),
format!(
"`{name}` is not part of the `#[schema(...)]` grammar, which is the keys of \
`kynos::schema::constraints::Constraints`: `minimum`, `maximum`, \
`exclusive_minimum`, `exclusive_maximum`, `multiple_of`, `min_length`, \
`max_length`, `pattern`, `min_items`, `max_items` and `unique_items`; plus \
`open`, which says a flattened field's members are not named"
),
))
}
fn reject_container_conversions(input: &DeriveInput) -> syn::Result<()> {
let Some((key, span)) = serde_key_span(&input.attrs, CONVERSIONS) else {
return Ok(());
};
let (noun, members) = match &input.data {
Data::Enum(_) => ("enum", "variants"),
Data::Struct(_) | Data::Union(_) => ("struct", "fields"),
};
Err(syn::Error::new(
span,
format!(
"`{key}` makes serde read or write this {noun} as the type it names rather than as \
the {members} it declares, so a schema derived from the declaration would describe \
a value the wire never carries. Implement `Schema` for this {noun} by hand, \
describing the type serde converts through -- `registry.resolve::<T>()` where that \
is one type `T` in both directions"
),
))
}
fn reject_untagged(input: &DeriveInput) -> syn::Result<()> {
let Data::Enum(data) = &input.data else {
return Ok(());
};
for attr in &input.attrs {
if !attr.path().is_ident("serde") {
continue;
}
let mut found = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("untagged") {
found = Some(meta.path.span());
} else {
skip_value(&meta)?;
}
Ok(())
});
if let Some(span) = found {
return Err(syn::Error::new(
span,
"an untagged enum has no describable decoding rule: `anyOf` without a \
discriminator is ambiguous, and serde's first-match tie-break cannot be \
expressed. Use `#[serde(tag = \"...\")]`, which becomes a `discriminator`",
));
}
}
for variant in described_variants(data) {
if let Some((_, span)) = serde_key_span(&variant.attrs, &["untagged"]) {
return Err(syn::Error::new(
span,
"an untagged variant has no describable decoding rule: serde writes it as its \
bare payload and reads it only once every tagged variant has failed, a \
first-match tie-break a `oneOf` cannot express. Tag the variant like its \
siblings, or publish the value as `Unchecked` on purpose",
));
}
}
Ok(())
}
fn reject_wire_form_overrides(input: &DeriveInput) -> syn::Result<()> {
type Scanned<'a> = (&'a [syn::Attribute], &'static str, &'static [&'static str]);
fn fields<'a>(
fields: &'a Fields,
newtype: bool,
keys: &'static [&'static str],
) -> Vec<Scanned<'a>> {
let scanned: fn(&&Field) -> bool = match fields {
Fields::Named(_) => |field| !serde_flag(&field.attrs, &["skip", "skip_deserializing"]),
Fields::Unnamed(_) if newtype => |_| true,
Fields::Unnamed(_) | Fields::Unit => |field| !is_skipped_both_ways(&field.attrs),
};
fields
.iter()
.filter(scanned)
.map(|field| {
let unwritten =
field.ident.is_some() && serde_flag(&field.attrs, &["skip_serializing"]);
let keys = if unwritten { READ_OVERRIDES } else { keys };
(field.attrs.as_slice(), "field", keys)
})
.collect()
}
fn picked(fields: &Fields) -> Vec<Scanned<'_>> {
fn scanned<'a>(field: &'a Field, keys: &'static [&'static str]) -> Scanned<'a> {
(field.attrs.as_slice(), "field", keys)
}
match transparent_picks(fields) {
(Some(written), Some(read)) if std::ptr::eq(written, read) => {
vec![scanned(written, WIRE_FORM_OVERRIDES)]
}
(written, read) => written
.map(|field| scanned(field, WRITE_OVERRIDES))
.into_iter()
.chain(read.map(|field| scanned(field, READ_OVERRIDES)))
.collect(),
}
}
let described = match &input.data {
Data::Struct(data) if Container::read(input).transparent => picked(&data.fields),
Data::Struct(data) => fields(&data.fields, data.fields.len() == 1, WIRE_FORM_OVERRIDES),
Data::Enum(data) => described_variants(data)
.into_iter()
.flat_map(|variant| {
let keys = if is_written(variant) {
WIRE_FORM_OVERRIDES
} else {
READ_OVERRIDES
};
let members = fields(&variant.fields, false, keys);
std::iter::once((variant.attrs.as_slice(), "variant", keys)).chain(members)
})
.collect(),
Data::Union(_) => Vec::new(),
};
for (attrs, noun, keys) in described {
if let Some((key, span)) = serde_key_span(attrs, keys) {
return Err(syn::Error::new(
span,
format!(
"`{key}` reads or writes this {noun} in a form its Rust type does not \
predict, so a schema derived from the type would describe a value the wire \
never carries. Give the value a newtype whose own `Serialize` and \
`Deserialize` produce that form and whose own `Schema` describes it"
),
));
}
}
Ok(())
}
fn reject_unread_variant(input: &DeriveInput) -> syn::Result<()> {
let Data::Enum(data) = &input.data else {
return Ok(());
};
for variant in described_variants(data) {
if let Some((_, span)) = serde_key_span(&variant.attrs, &["skip_deserializing"]) {
return Err(syn::Error::new(
span,
"`skip_deserializing` makes serde write this variant and refuse to read it back, \
so no closed `oneOf` or `enum` is true in both directions: listing the variant \
describes a request serde refuses, and leaving it out describes a response \
serde writes. Use `#[serde(skip)]` to leave it out both ways, or drop \
`skip_deserializing`",
));
}
}
Ok(())
}
fn reject_shadowed_variant(input: &DeriveInput) -> syn::Result<()> {
let Data::Enum(data) = &input.data else {
return Ok(());
};
let container = Container::read(input);
let Some((later, earlier)) = shadowed_variant(&described_variants(data), &container) else {
return Ok(());
};
Err(syn::Error::new(
later.ident.span(),
format!(
"serde reads `{name}`, this variant's own name, as `{earlier}`, the earlier variant \
that also claims it, so `{later}` goes on the wire under a name that reads back as \
`{earlier}`, and no schema describing `{later}` is true in both directions. Drop \
the `rename` or `alias` that gives both variants the name",
name = variant_name(later, &container),
earlier = earlier.ident,
later = later.ident,
),
))
}
fn reject_catch_all(input: &DeriveInput) -> syn::Result<()> {
let Data::Enum(data) = &input.data else {
return Ok(());
};
for variant in described_variants(data) {
if let Some((_, span)) = serde_key_span(&variant.attrs, &["other"]) {
return Err(syn::Error::new(
span,
"`#[serde(other)]` accepts every tag this enum does not name, and only OpenAPI \
3.2's `discriminator.defaultMapping` can say where those go, which this derive \
does not emit. Name every variant the API accepts, or publish the value as \
`Unchecked` on purpose",
));
}
}
Ok(())
}
fn reject_transparent_without_one_field(input: &DeriveInput) -> syn::Result<()> {
let Data::Struct(data) = &input.data else {
return Ok(());
};
let Some((_, span)) = serde_key_span(&input.attrs, &["transparent"]) else {
return Ok(());
};
let (Some(written), Some(read)) = transparent_picks(&data.fields) else {
return Ok(());
};
if std::ptr::eq(written, read) {
return Ok(());
}
let label = |member: &Field| {
member.ident.as_ref().map_or_else(
|| {
let index = data
.fields
.iter()
.position(|field| std::ptr::eq(field, member))
.unwrap_or_default();
format!("field {index}")
},
|ident| format!("`{ident}`"),
)
};
let (writes, reads) = (label(written), label(read));
Err(syn::Error::new(
span,
format!(
"`#[serde(transparent)]` makes serde write through the one field without `skip` or \
`skip_serializing` and read through the one field without `skip`, \
`skip_deserializing` or `default`, and `Schema` describes the struct only where \
they are the same field, since it does not compare two fields' schemas; this struct \
writes through {writes} and reads through {reads}. Leave \
one field serde both writes and reads, and mark every other `#[serde(skip)]`"
),
))
}
fn reject_read_required_skip(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
if container.transparent {
return Ok(());
}
let groups: Vec<&Fields> = match &input.data {
Data::Struct(data) => vec![&data.fields],
Data::Enum(data) => data
.variants
.iter()
.filter(|variant| is_written(variant))
.map(|variant| &variant.fields)
.collect(),
Data::Union(_) => Vec::new(),
};
let named = groups.into_iter().filter_map(|fields| match fields {
Fields::Named(named) => Some(&named.named),
Fields::Unnamed(_) | Fields::Unit => None,
});
for field in named.flatten().filter(|field| is_described(field)) {
let Some((key, span)) =
serde_key_span(&field.attrs, &["skip_serializing_if", "skip_serializing"])
else {
continue;
};
if is_flattened(field) {
if is_open(field) {
continue;
}
return Err(syn::Error::new(
span,
format!(
"`{key}` on a flattened field is refused unless it is \
`#[schema(open)]`. A flattened map must be `#[schema(open)]` for the schema \
to describe it, and may then skip itself, since serde reads it absent as \
empty. A flattened struct is written whole or not at all, so drop `{key}` \
to keep its members consistent with its schema. `#[serde(default)]` does \
not change this on a flattened field"
),
));
}
if !is_required(field, &container) {
continue;
}
return Err(syn::Error::new(
span,
format!(
"`{key}` lets serde leave this field out of what it writes, but without a \
`#[serde(default)]` on the field or its struct serde still requires it on \
read, so no `required` list is true in both directions. Add \
`#[serde(default)]` beside it or on the struct, or make the field an `Option`"
),
));
}
Ok(())
}
fn reject_contradicted_closure(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
if !container.deny_unknown_fields || container.transparent {
return Ok(());
}
let named = described_groups(input)
.into_iter()
.filter(|fields| matches!(fields, Fields::Named(_)))
.flat_map(described_members);
for field in named {
if let Some(span) = open_span(field).filter(|_| is_flattened(field)) {
return Err(syn::Error::new(
span,
"`#[schema(open)]` says this object admits members nothing names, but \
`#[serde(deny_unknown_fields)]` makes serde refuse every key its fields do not \
name before the map sees it, so serde reads the map empty and writes members it \
would refuse to read back. Drop `deny_unknown_fields` to keep the map, or drop \
the map",
));
}
}
Ok(())
}
fn struct_tag<'a>(input: &DeriveInput, container: &'a Container) -> Option<&'a str> {
let Data::Struct(data) = &input.data else {
return None;
};
if container.transparent || !matches!(data.fields, Fields::Named(_)) {
return None;
}
container.tag.as_deref()
}
fn reject_closed_tagged_struct(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
if struct_tag(input, &container).is_none() || !container.deny_unknown_fields {
return Ok(());
}
let span = serde_key_span(&input.attrs, &["deny_unknown_fields"])
.map_or_else(|| input.ident.span(), |(_, span)| span);
Err(syn::Error::new(
span,
"`#[serde(tag = \"...\")]` makes serde write the tag beside this struct's fields, but \
serde never reads it back as one of them, so `#[serde(deny_unknown_fields)]` refuses \
every document the struct writes, and no schema is true of both. Drop \
`deny_unknown_fields`, or drop the tag and declare it as a field",
))
}
fn reject_field_named_as_tag(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
let (Some(tag), Data::Struct(data)) = (struct_tag(input, &container), &input.data) else {
return Ok(());
};
let conflicting = data.fields.iter().find(|field| {
if is_flattened(field) || is_skipped_both_ways(&field.attrs) {
return false;
}
let written = !serde_flag(&field.attrs, &["skip_serializing"])
&& field_name(field, &container) == tag;
let read = !serde_flag(&field.attrs, &["skip_deserializing"])
&& aliases::read_names(field, &container)
.iter()
.any(|name| name == tag);
written || read
});
match conflicting.and_then(|field| field.ident.as_ref()) {
Some(ident) => Err(syn::Error::new(
ident.span(),
format!(
"`{tag}` is also this struct's `#[serde(tag = \"...\")]`, so serde writes the key \
twice, once as the tag and once as this field, and reads the tag's value back as \
the field. Rename the field or the tag, or `#[serde(skip)]` the field"
),
)),
None => Ok(()),
}
}
fn reject_unread_field_in_closed_object(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
if container.transparent || !container.deny_unknown_fields {
return Ok(());
}
for fields in written_groups(input) {
if let Some(span) = unread_field_span(fields) {
return Err(syn::Error::new(
span,
"`skip_deserializing` leaves this field out of the schema, since serde never \
reads it, but serde still writes it, and the `additionalProperties` or \
`unevaluatedProperties` of `false` that `#[serde(deny_unknown_fields)]` gives \
this object refuses a member the schema does not name. Use `#[serde(skip)]` to \
leave it out both ways, or drop `skip_deserializing` so the schema names it",
));
}
}
Ok(())
}
fn unread_field_span(fields: &Fields) -> Option<Span> {
let Fields::Named(named) = fields else {
return None;
};
named
.named
.iter()
.find_map(|field| one_way_skip_span(field, &["skip_deserializing"]))
.map(|(_, span)| span)
}
fn reject_one_way_member_skip(input: &DeriveInput) -> syn::Result<()> {
let container = Container::read(input);
if container.transparent {
return Ok(());
}
let groups: Vec<(&Punctuated<Field, Comma>, bool)> = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Unnamed(unnamed) if unnamed.unnamed.len() > 1 => {
vec![(&unnamed.unnamed, true)]
}
Fields::Named(_) | Fields::Unnamed(_) | Fields::Unit => Vec::new(),
},
Data::Enum(data) => described_variants(data)
.into_iter()
.filter_map(|variant| match &variant.fields {
Fields::Unnamed(unnamed) => Some((&unnamed.unnamed, is_written(variant))),
Fields::Named(_) | Fields::Unit => None,
})
.collect(),
Data::Union(_) => Vec::new(),
};
for (members, written) in groups {
let keys: &[&str] = if written {
&["skip_serializing", "skip_deserializing"]
} else {
&["skip_deserializing"]
};
let positions = positional_members(members);
for (index, field) in positions.iter().enumerate() {
if let Some((key, span)) = one_way_skip_span(field, keys) {
return Err(syn::Error::new(
span,
format!(
"`{key}` leaves this member out in one direction only, and a tuple \
position or a newtype variant's payload is on the wire or not as a \
whole, so serde would write one shape and read another. Use \
`#[serde(skip)]` to leave it out both ways, or give the type named \
fields"
),
));
}
if !written {
continue;
}
let Some((_, span)) = serde_key_span(&field.attrs, &["skip_serializing_if"]) else {
continue;
};
let defaulted = container.default || serde_flag(&field.attrs, &["default"]);
let last = index + 1 == positions.len();
if members.len() == 1 || (last && defaulted) {
continue;
}
return Err(syn::Error::new(
span,
"`skip_serializing_if` on a tuple member is refused unless it is the last \
described member and carries `#[serde(default)]`. serde leaves the member out \
of the array it writes, which moves every later member into its position, and \
reads the shorter array back only when a default fills the end. Move the member \
last beside `#[serde(default)]`, or give the type named fields",
));
}
}
Ok(())
}
fn reject_skipped_adjacent_payload(input: &DeriveInput) -> syn::Result<()> {
let Data::Enum(data) = &input.data else {
return Ok(());
};
let container = Container::read(input);
let (Some(_), Some(_)) = (&container.tag, &container.content) else {
return Ok(());
};
for variant in described_variants(data) {
let Fields::Unnamed(unnamed) = &variant.fields else {
continue;
};
let Some(member) = unnamed.unnamed.first() else {
continue;
};
if !is_unit_like(&variant.fields) || is_option(&member.ty) {
continue;
}
let keys = &["skip", "skip_serializing", "skip_deserializing"];
let Some((key, span)) = serde_key_span(&member.attrs, keys) else {
continue;
};
return Err(syn::Error::new(
span,
format!(
"`{key}` leaves out the only member of a newtype variant in an adjacently \
tagged enum, so serde writes the variant as its tag alone, but reads it back \
only with its content present, which nothing serde writes carries. Make the \
member an `Option`, which serde reads absent, or `#[serde(skip)]` the whole \
variant"
),
));
}
Ok(())
}
fn one_way_skip_span(field: &Field, keys: &[&str]) -> Option<(String, Span)> {
if is_skipped_both_ways(&field.attrs) {
return None;
}
serde_key_span(&field.attrs, keys)
}
fn positional_members(fields: &Punctuated<Field, Comma>) -> Vec<&Field> {
fields
.iter()
.filter(|field| !is_skipped_both_ways(&field.attrs))
.collect()
}
fn min_items(positions: &[&Field], defaulted: bool) -> u64 {
if defaulted {
return 0;
}
let required = positions
.iter()
.rposition(|field| !serde_flag(&field.attrs, &["default"]))
.map_or(0, |last| last + 1);
u64::try_from(required).unwrap_or(u64::MAX)
}
#[derive(Default)]
struct Container {
rename: Option<String>,
rename_all: Option<String>,
tag: Option<String>,
content: Option<String>,
transparent: bool,
doc: Option<String>,
default: bool,
deny_unknown_fields: bool,
}
impl Container {
fn read(input: &DeriveInput) -> Self {
let mut container = Self {
doc: doc_string(&input.attrs),
..Self::default()
};
for attr in &input.attrs {
if !attr.path().is_ident("serde") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
let Some(key) = meta.path.get_ident() else {
return skip_value(&meta);
};
match key.to_string().as_str() {
"rename" if meta.input.peek(syn::token::Paren) => {
meta.parse_nested_meta(|side| {
if side.path.is_ident("serialize") {
container.rename = string_value(&side)?;
return Ok(());
}
skip_value(&side)
})?;
}
"rename" => container.rename = string_value(&meta)?,
"rename_all" => container.rename_all = string_value(&meta)?,
"tag" => container.tag = string_value(&meta)?,
"content" => container.content = string_value(&meta)?,
"transparent" => container.transparent = true,
"deny_unknown_fields" => container.deny_unknown_fields = true,
_ => skip_value(&meta)?,
}
Ok(())
});
}
container.default = matches!(&input.data, Data::Struct(data) if !matches!(data.fields, Fields::Unit))
&& serde_flag(&input.attrs, &["default"]);
container
}
}
fn string_value(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<Option<String>> {
if !meta.input.peek(syn::Token![=]) {
return Ok(None);
}
Ok(Some(meta.value()?.parse::<LitStr>()?.value()))
}
fn body(input: &DeriveInput, container: &Container) -> TokenStream2 {
let described = match &input.data {
Data::Struct(data) => {
let name = container
.rename
.clone()
.unwrap_or_else(|| input.ident.unraw().to_string());
described(
struct_body(&data.fields, container, &name),
container.doc.as_deref(),
)
}
Data::Enum(data) => described(enum_body(data, container), container.doc.as_deref()),
Data::Union(_) => quote!(::kynos::openapi::Schema::default()),
};
deprecate(described, is_deprecated(&input.attrs))
}
fn deprecate(schema: TokenStream2, deprecated: bool) -> TokenStream2 {
if !deprecated {
return schema;
}
quote! {
{
let mut deprecated = #schema;
if let ::kynos::openapi::Schema::Object(keywords) = &mut deprecated {
keywords.deprecated = ::core::option::Option::Some(true);
}
deprecated
}
}
}
fn closed(schema: TokenStream2, container: &Container) -> TokenStream2 {
if !container.deny_unknown_fields {
return schema;
}
close(&schema)
}
fn close(schema: &TokenStream2) -> TokenStream2 {
quote! {
{
let mut closed = #schema;
if let ::kynos::openapi::Schema::Object(keywords) = &mut closed {
let never = ::core::option::Option::Some(::std::boxed::Box::new(
::kynos::openapi::Schema::never(),
));
if keywords.all_of.is_some() {
keywords.unevaluated_properties = never;
} else {
keywords.additional_properties = never;
}
}
closed
}
}
}
fn described(schema: TokenStream2, doc: Option<&str>) -> TokenStream2 {
let Some(doc) = doc else {
return schema;
};
quote! {
{
let mut described = #schema;
if let ::kynos::openapi::Schema::Object(keywords) = &mut described {
keywords.description =
::core::option::Option::Some(::std::string::String::from(#doc));
}
described
}
}
}
pub(super) fn property_names(input: &DeriveInput, fields: &syn::FieldsNamed) -> Vec<String> {
let container = Container::read(input);
fields
.named
.iter()
.map(|field| field_name(field, &container))
.collect()
}