diff --git a/.changepacks/changepack_log_1RyJaX5Qz8nf9_NR0htg3.json b/.changepacks/changepack_log_1RyJaX5Qz8nf9_NR0htg3.json new file mode 100644 index 0000000..6abe4cc --- /dev/null +++ b/.changepacks/changepack_log_1RyJaX5Qz8nf9_NR0htg3.json @@ -0,0 +1 @@ +{"changes":{"crates/vespera_macro/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch"},"note":"Support serde skip, default","date":"2025-12-12T06:23:26.149751200Z"} \ No newline at end of file diff --git a/crates/vespera_macro/src/openapi_generator.rs b/crates/vespera_macro/src/openapi_generator.rs index b371216..d444002 100644 --- a/crates/vespera_macro/src/openapi_generator.rs +++ b/crates/vespera_macro/src/openapi_generator.rs @@ -8,7 +8,10 @@ use vespera_core::{ }; use crate::metadata::CollectedMetadata; -use crate::parser::{build_operation_from_function, parse_enum_to_schema, parse_struct_to_schema}; +use crate::parser::{ + build_operation_from_function, extract_default, extract_field_rename, extract_rename_all, + parse_enum_to_schema, parse_struct_to_schema, rename_field, +}; /// Generate OpenAPI document from collected metadata pub fn generate_openapi_doc_with_metadata( @@ -33,12 +36,12 @@ pub fn generate_openapi_doc_with_metadata( // Then, parse all struct and enum schemas (now they can reference each other) for struct_meta in &metadata.structs { let parsed = syn::parse_str::(&struct_meta.definition).unwrap(); - let schema = match parsed { + let mut schema = match &parsed { syn::Item::Struct(struct_item) => { - parse_struct_to_schema(&struct_item, &known_schema_names, &struct_definitions) + parse_struct_to_schema(struct_item, &known_schema_names, &struct_definitions) } syn::Item::Enum(enum_item) => { - parse_enum_to_schema(&enum_item, &known_schema_names, &struct_definitions) + parse_enum_to_schema(enum_item, &known_schema_names, &struct_definitions) } _ => { // Fallback to struct parsing for backward compatibility @@ -49,6 +52,46 @@ pub fn generate_openapi_doc_with_metadata( ) } }; + + // Process default values for struct fields + if let syn::Item::Struct(struct_item) = &parsed { + // Find the file where this struct is defined + // Try to find a route file that contains this struct + let struct_file = metadata + .routes + .iter() + .find_map(|route| { + // Check if the file contains the struct definition + if let Ok(file_content) = std::fs::read_to_string(&route.file_path) { + // Check if the struct name appears in the file (more specific check) + // Look for "struct StructName" pattern + let struct_pattern = format!("struct {}", struct_meta.name); + if file_content.contains(&struct_pattern) { + return Some(route.file_path.clone()); + } + } + None + }) + .or_else(|| { + // Fallback: try all route files to find the struct + for route in &metadata.routes { + if let Ok(file_content) = std::fs::read_to_string(&route.file_path) { + let struct_pattern = format!("struct {}", struct_meta.name); + if file_content.contains(&struct_pattern) { + return Some(route.file_path.clone()); + } + } + } + // Last resort: use first route file if available + metadata.routes.first().map(|r| r.file_path.clone()) + }); + + if let Some(file_path) = struct_file && let Ok(file_content) = std::fs::read_to_string(&file_path) && let Ok(file_ast) = syn::parse_file(&file_content) { + // Process default functions for struct fields + process_default_functions(struct_item, &file_ast, &mut schema); + } + } + let schema_name = struct_meta.name.clone(); schemas.insert(schema_name.clone(), schema); } @@ -154,6 +197,212 @@ pub fn generate_openapi_doc_with_metadata( } } +/// Process default functions for struct fields +/// This function extracts default values from functions specified in #[serde(default = "function_name")] +fn process_default_functions( + struct_item: &syn::ItemStruct, + file_ast: &syn::File, + schema: &mut vespera_core::schema::Schema, +) { + use syn::Fields; + use vespera_core::schema::SchemaRef; + + // Extract rename_all from struct level + let struct_rename_all = extract_rename_all(&struct_item.attrs); + + // Get properties from schema + let properties = match &mut schema.properties { + Some(props) => props, + None => return, // No properties to process + }; + + // Process each field in the struct + if let Fields::Named(fields_named) = &struct_item.fields { + for field in &fields_named.named { + // Extract default function name + let default_info = match extract_default(&field.attrs) { + Some(Some(func_name)) => func_name, // default = "function_name" + Some(None) => { + // Simple default (no function) - we can set type-specific defaults + let rust_field_name = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) { + renamed + } else { + rename_field(&rust_field_name, struct_rename_all.as_deref()) + }; + + // Set type-specific default for simple default + if let Some(prop_schema_ref) = properties.get_mut(&field_name) + && let SchemaRef::Inline(prop_schema) = prop_schema_ref + && prop_schema.default.is_none() + && let Some(default_value) = get_type_default(&field.ty) + { + prop_schema.default = Some(default_value); + } + continue; + } + None => continue, // No default attribute + }; + + // Find the function in the file AST + let func = find_function_in_file(file_ast, &default_info); + if let Some(func_item) = func { + // Extract default value from function body + if let Some(default_value) = extract_default_value_from_function(func_item) { + // Get the field name (with rename applied) + let rust_field_name = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) { + renamed + } else { + rename_field(&rust_field_name, struct_rename_all.as_deref()) + }; + + // Set default value in schema + if let Some(prop_schema_ref) = properties.get_mut(&field_name) + && let SchemaRef::Inline(prop_schema) = prop_schema_ref + { + prop_schema.default = Some(default_value); + } + } + } + } + } +} + +/// Find a function by name in the file AST +fn find_function_in_file<'a>( + file_ast: &'a syn::File, + function_name: &str, +) -> Option<&'a syn::ItemFn> { + for item in &file_ast.items { + if let syn::Item::Fn(fn_item) = item + && fn_item.sig.ident == function_name + { + return Some(fn_item); + } + } + None +} + +/// Extract default value from function body +/// This tries to extract literal values from common patterns like: +/// - "value".to_string() -> "value" +/// - 42 -> 42 +/// - true -> true +/// - vec![] -> [] +fn extract_default_value_from_function(func: &syn::ItemFn) -> Option { + // Try to find return statement or expression + for stmt in &func.block.stmts { + if let syn::Stmt::Expr(expr, _) = stmt { + // Direct expression (like "value".to_string()) + if let Some(value) = extract_value_from_expr(expr) { + return Some(value); + } + // Or return statement + if let syn::Expr::Return(ret) = expr + && let Some(expr) = &ret.expr + && let Some(value) = extract_value_from_expr(expr) + { + return Some(value); + } + } + } + + None +} + +/// Extract value from expression +fn extract_value_from_expr(expr: &syn::Expr) -> Option { + use syn::{Expr, ExprLit, ExprMacro, Lit}; + + match expr { + // Literal values + Expr::Lit(ExprLit { lit, .. }) => match lit { + Lit::Str(s) => Some(serde_json::Value::String(s.value())), + Lit::Int(i) => { + if let Ok(val) = i.base10_parse::() { + Some(serde_json::Value::Number(val.into())) + } else { + None + } + } + Lit::Float(f) => { + if let Ok(val) = f.base10_parse::() { + Some(serde_json::Value::Number( + serde_json::Number::from_f64(val).unwrap_or(serde_json::Number::from(0)), + )) + } else { + None + } + } + Lit::Bool(b) => Some(serde_json::Value::Bool(b.value)), + _ => None, + }, + // Method calls like "value".to_string() + Expr::MethodCall(method_call) => { + if method_call.method == "to_string" { + // Get the receiver (the string literal) + // Try direct match first + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = method_call.receiver.as_ref() + { + return Some(serde_json::Value::String(s.value())); + } + // Try to extract from nested expressions (e.g., if the receiver is wrapped) + if let Some(value) = extract_value_from_expr(method_call.receiver.as_ref()) { + return Some(value); + } + } + None + } + // Macro calls like vec![] + Expr::Macro(ExprMacro { mac, .. }) => { + if mac.path.is_ident("vec") { + // Try to parse vec![] as empty array + return Some(serde_json::Value::Array(vec![])); + } + None + } + _ => None, + } +} + +/// Get type-specific default value for simple #[serde(default)] +fn get_type_default(ty: &syn::Type) -> Option { + use syn::Type; + match ty { + Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + match segment.ident.to_string().as_str() { + "String" => Some(serde_json::Value::String(String::new())), + "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => { + Some(serde_json::Value::Number(serde_json::Number::from(0))) + } + "f32" | "f64" => Some(serde_json::Value::Number( + serde_json::Number::from_f64(0.0).unwrap_or(serde_json::Number::from(0)), + )), + "bool" => Some(serde_json::Value::Bool(false)), + _ => None, + } + } else { + None + } + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/vespera_macro/src/parser/mod.rs b/crates/vespera_macro/src/parser/mod.rs index 52bde13..17fa36e 100644 --- a/crates/vespera_macro/src/parser/mod.rs +++ b/crates/vespera_macro/src/parser/mod.rs @@ -6,4 +6,7 @@ mod request_body; mod response; mod schema; pub use operation::build_operation_from_function; -pub use schema::{parse_enum_to_schema, parse_struct_to_schema}; +pub use schema::{ + extract_default, extract_field_rename, extract_rename_all, + parse_enum_to_schema, parse_struct_to_schema, rename_field, +}; diff --git a/crates/vespera_macro/src/parser/operation.rs b/crates/vespera_macro/src/parser/operation.rs index b8e4e1e..713cacb 100644 --- a/crates/vespera_macro/src/parser/operation.rs +++ b/crates/vespera_macro/src/parser/operation.rs @@ -302,14 +302,18 @@ mod tests { None => assert!(op.request_body.is_none()), Some(exp) => { let body = op.request_body.as_ref().expect("request body expected"); - let media = body.content.get(exp.content_type).or_else(|| { - // allow fallback to the only available content type if expected is absent - if body.content.len() == 1 { - body.content.values().next() - } else { - None - } - }).expect("expected content type"); + let media = body + .content + .get(exp.content_type) + .or_else(|| { + // allow fallback to the only available content type if expected is absent + if body.content.len() == 1 { + body.content.values().next() + } else { + None + } + }) + .expect("expected content type"); if let Some(schema_ty) = &exp.schema { match media.schema.as_ref().expect("schema expected") { SchemaRef::Inline(schema) => { diff --git a/crates/vespera_macro/src/parser/schema.rs b/crates/vespera_macro/src/parser/schema.rs index 1746c59..f8ddda4 100644 --- a/crates/vespera_macro/src/parser/schema.rs +++ b/crates/vespera_macro/src/parser/schema.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashMap}; use syn::{Fields, Type}; use vespera_core::schema::{Reference, Schema, SchemaRef, SchemaType}; -pub(super) fn extract_rename_all(attrs: &[syn::Attribute]) -> Option { +pub fn extract_rename_all(attrs: &[syn::Attribute]) -> Option { for attr in attrs { if attr.path().is_ident("serde") { // Parse the attribute tokens manually @@ -28,27 +28,188 @@ pub(super) fn extract_rename_all(attrs: &[syn::Attribute]) -> Option { None } -pub(super) fn extract_field_rename(attrs: &[syn::Attribute]) -> Option { +pub fn extract_field_rename(attrs: &[syn::Attribute]) -> Option { for attr in attrs { - if attr.path().is_ident("serde") { - // Try to parse as Meta::List first - if let syn::Meta::List(meta_list) = &attr.meta { - let tokens = meta_list.tokens.to_string(); - - // Look for rename = "..." pattern - if let Some(start) = tokens.find("rename") { - // Avoid false positives from rename_all - if tokens[start..].starts_with("rename_all") { - continue; - } - let remaining = &tokens[start + "rename".len()..]; - if let Some(equals_pos) = remaining.find('=') { - let value_part = &remaining[equals_pos + 1..].trim(); + if attr.path().is_ident("serde") + && let syn::Meta::List(meta_list) = &attr.meta + { + // Use parse_nested_meta to parse nested attributes + let mut found_rename = None; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") + && let Ok(value) = meta.value() + && let Ok(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(s), + .. + })) = value.parse::() + { + found_rename = Some(s.value()); + } + Ok(()) + }); + if let Some(rename_value) = found_rename { + return Some(rename_value); + } + + // Fallback: manual token parsing with regex-like approach + let tokens = meta_list.tokens.to_string(); + // Look for pattern: rename = "value" (with proper word boundaries) + if let Some(start) = tokens.find("rename") { + // Avoid false positives from rename_all + if tokens[start..].starts_with("rename_all") { + continue; + } + // Check that "rename" is a standalone word (not part of another word) + let before = if start > 0 { &tokens[..start] } else { "" }; + let after_start = start + "rename".len(); + let after = if after_start < tokens.len() { + &tokens[after_start..] + } else { + "" + }; + + let before_char = before.chars().last().unwrap_or(' '); + let after_char = after.chars().next().unwrap_or(' '); + + // Check if rename is a standalone word (preceded by space/comma/paren, followed by space/equals) + if (before_char == ' ' || before_char == ',' || before_char == '(') + && (after_char == ' ' || after_char == '=') + { + // Find the equals sign and extract the quoted value + if let Some(equals_pos) = after.find('=') { + let value_part = &after[equals_pos + 1..].trim(); // Extract string value (remove quotes) - if value_part.starts_with('"') && value_part.ends_with('"') { - let value = &value_part[1..value_part.len() - 1]; - return Some(value.to_string()); + if let Some(quote_start) = value_part.find('"') { + let after_quote = &value_part[quote_start + 1..]; + if let Some(quote_end) = after_quote.find('"') { + let value = &after_quote[..quote_end]; + return Some(value.to_string()); + } + } + } + } + } + } + } + None +} + +/// Extract skip attribute from field attributes +/// Returns true if #[serde(skip)] is present +pub(super) fn extract_skip(attrs: &[syn::Attribute]) -> bool { + for attr in attrs { + if attr.path().is_ident("serde") + && let syn::Meta::List(meta_list) = &attr.meta + { + let tokens = meta_list.tokens.to_string(); + // Check for "skip" (not part of skip_serializing_if or skip_deserializing) + if tokens.contains("skip") { + // Make sure it's not skip_serializing_if or skip_deserializing + if !tokens.contains("skip_serializing_if") && !tokens.contains("skip_deserializing") + { + // Check if it's a standalone "skip" + let skip_pos = tokens.find("skip"); + if let Some(pos) = skip_pos { + let before = if pos > 0 { &tokens[..pos] } else { "" }; + let after = &tokens[pos + "skip".len()..]; + // Check if skip is not part of another word + let before_char = before.chars().last().unwrap_or(' '); + let after_char = after.chars().next().unwrap_or(' '); + if (before_char == ' ' || before_char == ',' || before_char == '(') + && (after_char == ' ' || after_char == ',' || after_char == ')') + { + return true; + } + } + } + } + } + } + false +} + +/// Extract skip_serializing_if attribute from field attributes +/// Returns true if #[serde(skip_serializing_if = "...")] is present +pub fn extract_skip_serializing_if(attrs: &[syn::Attribute]) -> bool { + for attr in attrs { + if attr.path().is_ident("serde") + && let syn::Meta::List(meta_list) = &attr.meta + { + let mut found = false; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip_serializing_if") { + found = true; + } + Ok(()) + }); + if found { + return true; + } + + // Fallback: check tokens string + let tokens = meta_list.tokens.to_string(); + if tokens.contains("skip_serializing_if") { + return true; + } + } + } + false +} + +/// Extract default attribute from field attributes +/// Returns: +/// - Some(None) if #[serde(default)] is present (no function) +/// - Some(Some(function_name)) if #[serde(default = "function_name")] is present +/// - None if no default attribute is present +pub fn extract_default(attrs: &[syn::Attribute]) -> Option> { + for attr in attrs { + if attr.path().is_ident("serde") + && let syn::Meta::List(meta_list) = &attr.meta + { + let mut found_default: Option> = None; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("default") { + // Check if it has a value (default = "function_name") + if let Ok(value) = meta.value() { + if let Ok(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(s), + .. + })) = value.parse::() + { + found_default = Some(Some(s.value())); } + } else { + // Just "default" without value + found_default = Some(None); + } + } + Ok(()) + }); + if let Some(default_value) = found_default { + return Some(default_value); + } + + // Fallback: manual token parsing + let tokens = meta_list.tokens.to_string(); + if let Some(start) = tokens.find("default") { + let remaining = &tokens[start + "default".len()..]; + if remaining.trim_start().starts_with('=') { + // default = "function_name" + let value_part = remaining.trim_start()[1..].trim(); + if value_part.starts_with('"') && value_part.ends_with('"') { + let function_name = &value_part[1..value_part.len() - 1]; + return Some(Some(function_name.to_string())); + } + } else { + // Just "default" without = (standalone) + let before = if start > 0 { &tokens[..start] } else { "" }; + let after = &remaining; + let before_char = before.chars().last().unwrap_or(' '); + let after_char = after.chars().next().unwrap_or(' '); + if (before_char == ' ' || before_char == ',' || before_char == '(') + && (after_char == ' ' || after_char == ',' || after_char == ')') + { + return Some(None); } } } @@ -57,7 +218,7 @@ pub(super) fn extract_field_rename(attrs: &[syn::Attribute]) -> Option { None } -pub(super) fn rename_field(field_name: &str, rename_all: Option<&str>) -> String { +pub fn rename_field(field_name: &str, rename_all: Option<&str>) -> String { // "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE" match rename_all { Some("camelCase") => { @@ -400,6 +561,11 @@ pub fn parse_struct_to_schema( match &struct_item.fields { Fields::Named(fields_named) => { for field in &fields_named.named { + // Check if field should be skipped + if extract_skip(&field.attrs) { + continue; + } + let rust_field_name = field .ident .as_ref() @@ -416,26 +582,43 @@ pub fn parse_struct_to_schema( let field_type = &field.ty; - let schema_ref = + let mut schema_ref = parse_type_to_schema_ref(field_type, known_schemas, struct_definitions); - properties.insert(field_name.clone(), schema_ref); - - // Check if field is Option - let is_optional = matches!( - field_type, - Type::Path(type_path) - if type_path - .path - .segments - .first() - .map(|s| s.ident == "Option") - .unwrap_or(false) - ); - - if !is_optional { - required.push(field_name); + // Check for default attribute + let has_default = extract_default(&field.attrs).is_some(); + + // Check for skip_serializing_if attribute + let has_skip_serializing_if = extract_skip_serializing_if(&field.attrs); + + // If default or skip_serializing_if is present, mark field as optional (not required) + // and set default value if it's a simple default (not a function) + if has_default || has_skip_serializing_if { + // For default = "function_name", we'll handle it in openapi_generator + // For now, just mark as optional + if let SchemaRef::Inline(ref mut _schema) = schema_ref { + // Default will be set later in openapi_generator if it's a function + // For simple default, we could set it here, but serde handles it + } + } else { + // Check if field is Option + let is_optional = matches!( + field_type, + Type::Path(type_path) + if type_path + .path + .segments + .first() + .map(|s| s.ident == "Option") + .unwrap_or(false) + ); + + if !is_optional { + required.push(field_name.clone()); + } } + + properties.insert(field_name, schema_ref); } } Fields::Unnamed(_) => { diff --git a/examples/axum-example/openapi.json b/examples/axum-example/openapi.json index e4d39a8..013e241 100644 --- a/examples/axum-example/openapi.json +++ b/examples/axum-example/openapi.json @@ -934,6 +934,23 @@ } } }, + "/users/skip-response": { + "get": { + "operationId": "skip_response", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkipResponse" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", @@ -1527,6 +1544,37 @@ "createdAt" ] }, + "SkipResponse": { + "type": "object", + "properties": { + "email2": { + "type": "string", + "nullable": true + }, + "email4": { + "type": "string", + "nullable": true + }, + "email5": { + "type": "string", + "default": "" + }, + "email6": { + "type": "string", + "default": "default42" + }, + "name": { + "type": "string" + }, + "num": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "name" + ] + }, "StructBody": { "type": "object", "properties": { diff --git a/examples/axum-example/src/routes/users.rs b/examples/axum-example/src/routes/users.rs index d1bd81f..a4f4e33 100644 --- a/examples/axum-example/src/routes/users.rs +++ b/examples/axum-example/src/routes/users.rs @@ -53,3 +53,54 @@ pub async fn create_user(Json(user): Json) -> Json { email: user.email, }) } + +#[derive(Serialize, Deserialize, Schema)] +pub struct SkipResponse { + pub name: String, + #[serde(skip)] + #[allow(dead_code)] + pub email: String, + + #[serde(skip, skip_serializing_if = "Option::is_none")] + #[allow(dead_code)] + pub email2: Option, + + #[serde(rename = "email3", skip)] + #[allow(dead_code)] + pub email3: Option, + + #[serde(rename = "email4", skip_serializing_if = "Option::is_none")] + pub email4: Option, + + #[serde(rename = "email5", default)] + pub email5: String, + + #[serde(rename = "email6", default = "default_value")] + pub email6: String, + + #[serde(rename = "email7", skip)] + #[allow(dead_code)] + pub email7: String, + + #[serde(rename = "num", default)] + pub num: i32, +} + +fn default_value() -> String { + "default42".to_string() +} + +#[vespera::route(get, path = "/skip-response")] +pub async fn skip_response() -> Json { + Json(SkipResponse { + name: "John Doe".to_string(), + email: "john.doe@example.com".to_string(), + email2: Some("john.doe2@example.com".to_string()), + email3: Some("john.doe3@example.com".to_string()), + email4: Some("john.doe4@example.com".to_string()), + email5: "john.doe5@example.com".to_string(), + email6: "john.doe6@example.com".to_string(), + email7: "john.doe7@example.com".to_string(), + num: 0, + }) +} diff --git a/examples/axum-example/tests/snapshots/integration_test__openapi.snap b/examples/axum-example/tests/snapshots/integration_test__openapi.snap index d4c598e..2f50579 100644 --- a/examples/axum-example/tests/snapshots/integration_test__openapi.snap +++ b/examples/axum-example/tests/snapshots/integration_test__openapi.snap @@ -938,6 +938,23 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } } }, + "/users/skip-response": { + "get": { + "operationId": "skip_response", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkipResponse" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", @@ -1531,6 +1548,37 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "createdAt" ] }, + "SkipResponse": { + "type": "object", + "properties": { + "email2": { + "type": "string", + "nullable": true + }, + "email4": { + "type": "string", + "nullable": true + }, + "email5": { + "type": "string", + "default": "" + }, + "email6": { + "type": "string", + "default": "default42" + }, + "name": { + "type": "string" + }, + "num": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "name" + ] + }, "StructBody": { "type": "object", "properties": { diff --git a/openapi.json b/openapi.json index e4d39a8..013e241 100644 --- a/openapi.json +++ b/openapi.json @@ -934,6 +934,23 @@ } } }, + "/users/skip-response": { + "get": { + "operationId": "skip_response", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkipResponse" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", @@ -1527,6 +1544,37 @@ "createdAt" ] }, + "SkipResponse": { + "type": "object", + "properties": { + "email2": { + "type": "string", + "nullable": true + }, + "email4": { + "type": "string", + "nullable": true + }, + "email5": { + "type": "string", + "default": "" + }, + "email6": { + "type": "string", + "default": "default42" + }, + "name": { + "type": "string" + }, + "num": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "name" + ] + }, "StructBody": { "type": "object", "properties": {