From 5a28cef598375530f8abfa0678b057fee6b03f85 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Sat, 16 May 2020 21:16:59 +0100 Subject: [PATCH] Respect #[serde(transparent)] attribute (#17) --- CHANGELOG.md | 1 + docs/1.1-attributes.md | 9 ++++ .../tests/expected/transparent-struct.json | 33 ++++++++++++++ schemars/tests/transparent.rs | 27 ++++++++++++ schemars_derive/src/ast/mod.rs | 10 +++++ schemars_derive/src/attr/schemars_to_serde.rs | 1 + schemars_derive/src/lib.rs | 43 +++++++++++++++++-- schemars_derive/src/schema_exprs.rs | 2 +- 8 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 schemars/tests/expected/transparent-struct.json create mode 100644 schemars/tests/transparent.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ec122..a95ed2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## In-dev - version TBC ### Added: - Setting `#[deprecated]` attribute will now cause generated schemas to have the `deprecated` property set to `true` +- Respect #[serde(transparent)] attribute (https://github.com/GREsau/schemars/issues/17) ## [0.7.4] - 2020-05-16 ### Added: diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index 47ace02..6c509d9 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -133,6 +133,15 @@ Setting this on a container will set the `additionalProperties` keyword on gener Serde docs: [container](https://serde.rs/container-attrs.html#deny_unknown_fields) +

+ +`#[serde(transparent)]` / `#[schemars(transparent)]` +

+ +Set on a newtype struct or a braced struct with one field to make the struct's generated schema exactly the same as that of the single field's. + +Serde docs: [container](https://serde.rs/container-attrs.html#transparent) + ## Other Attributes diff --git a/schemars/tests/expected/transparent-struct.json b/schemars/tests/expected/transparent-struct.json new file mode 100644 index 0000000..e83f905 --- /dev/null +++ b/schemars/tests/expected/transparent-struct.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OuterStruct", + "type": "object", + "properties": { + "inner": { + "anyOf": [ + { + "$ref": "#/definitions/InnerStruct" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "InnerStruct": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + } + } +} \ No newline at end of file diff --git a/schemars/tests/transparent.rs b/schemars/tests/transparent.rs new file mode 100644 index 0000000..7bbb24a --- /dev/null +++ b/schemars/tests/transparent.rs @@ -0,0 +1,27 @@ +mod util; +use schemars::JsonSchema; +use util::*; + +#[derive(Debug, JsonSchema)] +pub struct OuterStruct { + inner: TransparentStruct, +} + +#[derive(Debug, JsonSchema)] +#[serde(transparent)] +pub struct TransparentStruct { + #[serde(with = "TransparentNewType")] + inner: (), +} + +#[derive(Debug, JsonSchema)] +#[schemars(transparent)] +pub struct TransparentNewType(Option); + +#[derive(Debug, JsonSchema)] +pub struct InnerStruct(String, i32); + +#[test] +fn transparent_struct() -> TestResult { + test_default_generated_schema::("transparent-struct") +} diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 2f2cb98..a388ac8 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -49,6 +49,16 @@ impl<'a> Container<'a> { pub fn name(&self) -> String { self.serde_attrs.name().deserialize_name() } + + pub fn transparent_field(&'a self) -> Option<&'a Field> { + if self.serde_attrs.transparent() { + if let Data::Struct(_, fields) = &self.data { + return Some(&fields[0]); + } + } + + None + } } impl<'a> Variant<'a> { diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 20b7b10..380a276 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -19,6 +19,7 @@ static SERDE_KEYWORDS: &[&str] = &[ "skip_deserializing", "flatten", "remote", + "transparent", // Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars attrs to serde attrs. // This is because we want to preserve any serde attribute's `serialize_with` value to determine whether the field's // default value should be serialized. We also check the `with` value on schemars/serde attrs e.g. to support deriving diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 2e768d4..006d78a 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -30,10 +30,44 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream { Err(e) => return compile_error(&e), }; - let schema_expr = schema_exprs::expr_for_container(&cont); - let type_name = &cont.ident; - let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect(); + let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl(); + + if let Some(transparent_field) = cont.transparent_field() { + let (ty, type_def) = schema_exprs::type_for_schema(transparent_field, 0); + return quote! { + #[automatically_derived] + impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause { + #type_def + + fn is_referenceable() -> bool { + <#ty as schemars::JsonSchema>::is_referenceable() + } + + fn schema_name() -> std::string::String { + <#ty as schemars::JsonSchema>::schema_name() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + <#ty as schemars::JsonSchema>::json_schema(gen) + } + + fn json_schema_for_flatten(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + <#ty as schemars::JsonSchema>::json_schema_for_flatten(gen) + } + + fn add_schema_as_property( + gen: &mut schemars::gen::SchemaGenerator, + parent: &mut schemars::schema::SchemaObject, + name: String, + metadata: Option, + required: bool, + ) { + <#ty as schemars::JsonSchema>::add_schema_as_property(gen, parent, name, metadata, required) + } + }; + }; + } let mut schema_base_name = cont.name(); let schema_is_renamed = *type_name != schema_base_name; @@ -46,6 +80,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream { } } + let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect(); let schema_name = if type_params.is_empty() { quote! { #schema_base_name.to_owned() @@ -67,7 +102,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream { } }; - let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl(); + let schema_expr = schema_exprs::expr_for_container(&cont); quote! { #[automatically_derived] diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 7129c02..6b312fe 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -38,7 +38,7 @@ fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream { } } -fn type_for_schema(field: &Field, local_id: usize) -> (syn::Type, Option) { +pub fn type_for_schema(field: &Field, local_id: usize) -> (syn::Type, Option) { match &field.attrs.with { None => (field.ty.to_owned(), None), Some(WithAttr::Type(ty)) => (ty.to_owned(), None),