Skip to content

Commit

Permalink
Support serde deny_unknown_fields (#816)
Browse files Browse the repository at this point in the history
  • Loading branch information
jayvdb authored Dec 10, 2023
1 parent 7e49344 commit f965165
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 6 deletions.
2 changes: 1 addition & 1 deletion utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ulid = { version = "1", optional = true, default-features = false }
url = { version = "2", optional = true }

[dev-dependencies]
utoipa = { path = "../utoipa", features = ["uuid"], default-features = false }
utoipa = { path = "../utoipa", features = ["debug", "uuid"], default-features = false }
serde_json = "1"
serde = "1"
actix-web = { version = "4", features = ["macros"], default-features = false }
Expand Down
22 changes: 18 additions & 4 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ impl ToTokens for NamedStructSchema<'_> {
})
.collect();

if !flatten_fields.is_empty() {
let all_of = if !flatten_fields.is_empty() {
let mut flattened_tokens = TokenStream::new();
let mut flattened_map_field = None;
for field in flatten_fields {
Expand Down Expand Up @@ -504,16 +504,30 @@ impl ToTokens for NamedStructSchema<'_> {
}

if flattened_tokens.is_empty() {
tokens.extend(object_tokens)
tokens.extend(object_tokens);
false
} else {
tokens.extend(quote! {
utoipa::openapi::AllOfBuilder::new()
#flattened_tokens
.item(#object_tokens)
})
});
true
}
} else {
tokens.extend(object_tokens)
tokens.extend(object_tokens);
false
};

if !all_of
&& container_rules
.as_ref()
.and_then(|container_rule| Some(container_rule.deny_unknown_fields))
.unwrap_or(false)
{
tokens.extend(quote! {
.additional_properties(Some(utoipa::openapi::schema::AdditionalProperties::FreeForm(false)))
});
}

if let Some(deprecated) = super::get_deprecated(self.attributes) {
Expand Down
46 changes: 45 additions & 1 deletion utoipa-gen/src/component/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> {

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct SerdeValue {
pub skip: bool,
pub rename: Option<String>,
Expand Down Expand Up @@ -89,6 +90,7 @@ impl SerdeValue {
/// The default case (when no serde attributes are present) is `ExternallyTagged`.
#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum SerdeEnumRepr {
ExternallyTagged,
InternallyTagged {
Expand Down Expand Up @@ -116,10 +118,12 @@ impl Default for SerdeEnumRepr {
/// Attributes defined within a `#[serde(...)]` container attribute.
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct SerdeContainer {
pub rename_all: Option<RenameRule>,
pub enum_repr: SerdeEnumRepr,
pub default: bool,
pub deny_unknown_fields: bool,
}

impl SerdeContainer {
Expand All @@ -129,6 +133,7 @@ impl SerdeContainer {
/// * `content = ...`
/// * `untagged = ...`
/// * `default = ...`
/// * `deny_unknown_fields`
fn parse_attribute(&mut self, ident: Ident, next: Cursor) -> syn::Result<()> {
match ident.to_string().as_str() {
"rename_all" => {
Expand Down Expand Up @@ -188,6 +193,9 @@ impl SerdeContainer {
"default" => {
self.default = true;
}
"deny_unknown_fields" => {
self.deny_unknown_fields = true;
}
_ => {}
}
Ok(())
Expand Down Expand Up @@ -262,6 +270,9 @@ pub fn parse_container(attributes: &[Attribute]) -> Option<SerdeContainer> {
if value.default {
acc.default = value.default;
}
if value.deny_unknown_fields {
acc.deny_unknown_fields = value.deny_unknown_fields;
}
match value.enum_repr {
SerdeEnumRepr::ExternallyTagged => {}
SerdeEnumRepr::Untagged
Expand All @@ -282,6 +293,7 @@ pub fn parse_container(attributes: &[Attribute]) -> Option<SerdeContainer> {

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum RenameRule {
Lower,
Upper,
Expand Down Expand Up @@ -395,7 +407,8 @@ impl FromStr for RenameRule {

#[cfg(test)]
mod tests {
use super::{RenameRule, RENAME_RULE_NAME_MAPPING};
use super::{parse_container, RenameRule, SerdeContainer, RENAME_RULE_NAME_MAPPING};
use syn::{parse_quote, Attribute};

macro_rules! test_rename_rule {
( $($case:expr=> $value:literal = $expected:literal)* ) => {
Expand Down Expand Up @@ -467,4 +480,35 @@ mod tests {
s.parse::<RenameRule>().unwrap();
}
}

#[test]
fn test_serde_parse_container() {
let default_attribute_1: syn::Attribute = parse_quote! {
#[serde(default)]
};
let default_attribute_2: syn::Attribute = parse_quote! {
#[serde(default)]
};
let deny_unknown_fields_attribute: syn::Attribute = parse_quote! {
#[serde(deny_unknown_fields)]
};
let unsupported_attribute: syn::Attribute = parse_quote! {
#[serde(expecting = "...")]
};
let attributes: &[Attribute] = &[
default_attribute_1,
default_attribute_2,
deny_unknown_fields_attribute,
unsupported_attribute,
];

let expected = SerdeContainer {
default: true,
deny_unknown_fields: true,
..Default::default()
};

let result = parse_container(attributes).unwrap();
assert_eq!(expected, result);
}
}
1 change: 1 addition & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ use self::{
/// * `untagged` Supported at the container level. Allows [untagged
/// enum representation](https://serde.rs/enum-representations.html#untagged).
/// * `default` Supported at the container level and field level according to [serde attributes].
/// * `deny_unknown_fields` Supported at the container level.
/// * `flatten` Supported at the field level.
///
/// Other _`serde`_ attributes works as is but does not have any effect on the generated OpenAPI doc.
Expand Down
25 changes: 25 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,31 @@ fn derive_schema_with_default_struct() {
)
}

#[test]
fn derive_struct_with_no_additional_properties() {
let value = api_doc! {
#[derive(serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct MyValue {
field: String
}
};

assert_json_eq!(
value,
json!({
"properties": {
"field": {
"type": "string",
}
},
"required": ["field"],
"additionalProperties": false,
"type": "object"
})
)
}

#[test]
#[cfg(feature = "repr")]
fn derive_schema_for_repr_enum() {
Expand Down

0 comments on commit f965165

Please sign in to comment.