@@ -51,13 +51,15 @@ use std::{any::type_name, marker::PhantomData};
5151use crate :: {
5252 generate:: GenContext ,
5353 openapi:: {
54- Components , Contact , Info , License , OpenApi , Operation , Parameter , PathItem , ReferenceOr ,
55- Response , SecurityScheme , Server , StatusCode , Tag ,
54+ Components , Contact , Info , License , OpenApi , Operation , Parameter ,
55+ ParameterSchemaOrContent , PathItem , ReferenceOr , Response , SecurityScheme , Server ,
56+ StatusCode , Tag ,
5657 } ,
5758 OperationInput ,
5859} ;
5960use indexmap:: IndexMap ;
6061use serde:: Serialize ;
62+ use serde_json:: Value ;
6163
6264use crate :: {
6365 error:: Error , generate:: in_context, operation:: OperationOutput , util:: iter_operations_mut,
@@ -194,6 +196,23 @@ impl<'t> TransformOpenApi<'t> {
194196 self
195197 }
196198
199+ /// Strip null types from query parameter schemas.
200+ ///
201+ /// Query strings cannot express null values - a parameter is either
202+ /// present with a value or absent. This method removes `null` from
203+ /// type arrays (e.g., `["string", "null"]` becomes `"string"`) and
204+ /// unwraps `anyOf` variants containing null types.
205+ ///
206+ /// Note: This is called automatically when the transform is finalized
207+ /// (unless disabled via [`aide::generate::strip_query_null_types`]).
208+ /// You only need to call this explicitly if you want to apply it
209+ /// at a specific point in your transform chain.
210+ #[ tracing:: instrument( skip_all) ]
211+ pub fn strip_null_from_query_params ( self ) -> Self {
212+ strip_null_from_query_params_impl ( self . api ) ;
213+ self
214+ }
215+
197216 /// Add a security scheme.
198217 #[ allow( clippy:: missing_panics_doc) ]
199218 pub fn security_scheme ( mut self , name : & str , scheme : SecurityScheme ) -> Self {
@@ -1320,3 +1339,224 @@ impl<'t> TransformCallback<'t> {
13201339fn filter_no_duplicate_response ( err : & Error ) -> bool {
13211340 !matches ! ( err, Error :: DefaultResponseExists | Error :: ResponseExists ( _) )
13221341}
1342+
1343+ pub ( crate ) fn strip_null_from_query_params_impl ( api : & mut OpenApi ) {
1344+ let Some ( paths) = & mut api. paths else { return } ;
1345+
1346+ for ( _, path_item) in & mut paths. paths {
1347+ let ReferenceOr :: Item ( path_item) = path_item else {
1348+ continue ;
1349+ } ;
1350+
1351+ for ( _, op) in iter_operations_mut ( path_item) {
1352+ for param in & mut op. parameters {
1353+ let ReferenceOr :: Item ( Parameter :: Query { parameter_data, .. } ) = param else {
1354+ continue ;
1355+ } ;
1356+
1357+ let ParameterSchemaOrContent :: Schema ( schema_obj) = & mut parameter_data. format
1358+ else {
1359+ continue ;
1360+ } ;
1361+
1362+ strip_null_from_type ( & mut schema_obj. json_schema ) ;
1363+ }
1364+ }
1365+ }
1366+ }
1367+
1368+ fn strip_null_from_type ( schema : & mut schemars:: Schema ) {
1369+ // Handle type: ["string", "null"] -> type: "string"
1370+ if let Some ( Value :: Array ( types) ) = schema. get_mut ( "type" ) {
1371+ let null_count = types. iter ( ) . filter ( |t| * t == "null" ) . count ( ) ;
1372+ if null_count == 0 || null_count == types. len ( ) {
1373+ return ; // No nulls, or all nulls - don't modify
1374+ }
1375+ types. retain ( |t| t != "null" ) ;
1376+ if types. len ( ) == 1 {
1377+ * schema. get_mut ( "type" ) . unwrap ( ) = types. remove ( 0 ) ;
1378+ }
1379+ return ;
1380+ }
1381+
1382+ // Handle anyOf: [..., {type: "null"}] -> remove null variants
1383+ let Some ( Value :: Array ( items) ) = schema. get_mut ( "anyOf" ) else {
1384+ return ;
1385+ } ;
1386+
1387+ let is_null = |v : & Value | matches ! ( v. get( "type" ) , Some ( Value :: String ( s) ) if s == "null" ) ;
1388+ let null_count = items. iter ( ) . filter ( |item| is_null ( item) ) . count ( ) ;
1389+ if null_count == 0 || null_count == items. len ( ) {
1390+ return ; // No nulls, or all nulls - don't modify
1391+ }
1392+
1393+ items. retain ( |item| !is_null ( item) ) ;
1394+
1395+ // Single item remains: unwrap the anyOf
1396+ if items. len ( ) == 1 {
1397+ if let Some ( Value :: Object ( obj) ) = items. pop ( ) {
1398+ if let Some ( schema_obj) = schema. as_object_mut ( ) {
1399+ schema_obj. remove ( "anyOf" ) ;
1400+ for ( key, value) in obj {
1401+ schema_obj. insert ( key, value) ;
1402+ }
1403+ }
1404+ }
1405+ }
1406+ }
1407+
1408+ #[ cfg( test) ]
1409+ mod tests {
1410+ use crate :: openapi:: {
1411+ MediaType , OpenApi , Operation , Parameter , ParameterData , ParameterSchemaOrContent , Paths ,
1412+ ReferenceOr , RequestBody , SchemaObject ,
1413+ } ;
1414+ use indexmap:: IndexMap ;
1415+ use schemars:: JsonSchema ;
1416+ use serde_json:: json;
1417+
1418+ use super :: TransformOpenApi ;
1419+
1420+ fn inline_schema_for < T : JsonSchema > ( ) -> schemars:: Schema {
1421+ let settings = schemars:: generate:: SchemaSettings :: draft07 ( ) . with ( |s| {
1422+ s. inline_subschemas = true ;
1423+ } ) ;
1424+ let mut gen = settings. into_generator ( ) ;
1425+ gen. subschema_for :: < T > ( )
1426+ }
1427+
1428+ fn property_schema ( struct_schema : & schemars:: Schema , name : & str ) -> schemars:: Schema {
1429+ struct_schema
1430+ . get ( "properties" )
1431+ . and_then ( |p| p. get ( name) )
1432+ . unwrap_or_else ( || panic ! ( "property {name:?} not found" ) )
1433+ . clone ( )
1434+ . try_into ( )
1435+ . unwrap ( )
1436+ }
1437+
1438+ fn build_api ( params : Vec < Parameter > , body_schema : Option < schemars:: Schema > ) -> OpenApi {
1439+ let mut op = Operation :: default ( ) ;
1440+ op. parameters = params. into_iter ( ) . map ( ReferenceOr :: Item ) . collect ( ) ;
1441+ if let Some ( schema) = body_schema {
1442+ op. request_body = Some ( ReferenceOr :: Item ( RequestBody {
1443+ content : IndexMap :: from_iter ( [ (
1444+ "application/json" . into ( ) ,
1445+ MediaType {
1446+ schema : Some ( SchemaObject {
1447+ json_schema : schema,
1448+ external_docs : None ,
1449+ example : None ,
1450+ } ) ,
1451+ ..Default :: default ( )
1452+ } ,
1453+ ) ] ) ,
1454+ ..Default :: default ( )
1455+ } ) ) ;
1456+ }
1457+
1458+ let mut path_item = crate :: openapi:: PathItem :: default ( ) ;
1459+ path_item. get = Some ( op) ;
1460+
1461+ OpenApi {
1462+ paths : Some ( Paths {
1463+ paths : IndexMap :: from ( [ ( "/test" . to_string ( ) , ReferenceOr :: Item ( path_item) ) ] ) ,
1464+ extensions : IndexMap :: new ( ) ,
1465+ } ) ,
1466+ ..OpenApi :: default ( )
1467+ }
1468+ }
1469+
1470+ fn query_param ( name : & str , schema : schemars:: Schema ) -> Parameter {
1471+ Parameter :: Query {
1472+ parameter_data : ParameterData {
1473+ name : name. to_string ( ) ,
1474+ description : None ,
1475+ required : false ,
1476+ deprecated : None ,
1477+ format : ParameterSchemaOrContent :: Schema ( SchemaObject {
1478+ json_schema : schema,
1479+ external_docs : None ,
1480+ example : None ,
1481+ } ) ,
1482+ example : None ,
1483+ examples : IndexMap :: new ( ) ,
1484+ explode : None ,
1485+ extensions : IndexMap :: new ( ) ,
1486+ } ,
1487+ allow_reserved : false ,
1488+ style : Default :: default ( ) ,
1489+ allow_empty_value : None ,
1490+ }
1491+ }
1492+
1493+ fn get_param_schema ( api : & OpenApi , param_index : usize ) -> & schemars:: Schema {
1494+ let paths = api. paths . as_ref ( ) . unwrap ( ) ;
1495+ let ReferenceOr :: Item ( path_item) = & paths. paths [ "/test" ] else {
1496+ panic ! ( "expected item" ) ;
1497+ } ;
1498+ let op = path_item. get . as_ref ( ) . unwrap ( ) ;
1499+ let ReferenceOr :: Item ( param) = & op. parameters [ param_index] else {
1500+ panic ! ( "expected parameter item" ) ;
1501+ } ;
1502+ let ParameterSchemaOrContent :: Schema ( schema_obj) = & param. parameter_data_ref ( ) . format
1503+ else {
1504+ panic ! ( "expected schema" ) ;
1505+ } ;
1506+ & schema_obj. json_schema
1507+ }
1508+
1509+ fn get_body_schema ( api : & OpenApi ) -> & schemars:: Schema {
1510+ let paths = api. paths . as_ref ( ) . unwrap ( ) ;
1511+ let ReferenceOr :: Item ( path_item) = & paths. paths [ "/test" ] else {
1512+ panic ! ( "expected item" ) ;
1513+ } ;
1514+ let op = path_item. get . as_ref ( ) . unwrap ( ) ;
1515+ let Some ( ReferenceOr :: Item ( body) ) = & op. request_body else {
1516+ panic ! ( "expected request body" ) ;
1517+ } ;
1518+ & body. content [ "application/json" ]
1519+ . schema
1520+ . as_ref ( )
1521+ . unwrap ( )
1522+ . json_schema
1523+ }
1524+
1525+ #[ test]
1526+ fn strip_null_from_query_params ( ) {
1527+ #[ derive( JsonSchema ) ]
1528+ #[ allow( dead_code) ]
1529+ struct QueryParams {
1530+ optional : Option < String > ,
1531+ }
1532+
1533+ #[ derive( JsonSchema ) ]
1534+ #[ allow( dead_code) ]
1535+ struct Body {
1536+ optional : Option < String > ,
1537+ }
1538+
1539+ let query_field = property_schema ( & inline_schema_for :: < QueryParams > ( ) , "optional" ) ;
1540+ let body_field = property_schema ( & inline_schema_for :: < Body > ( ) , "optional" ) ;
1541+
1542+ // Both should start out nullable
1543+ assert_eq ! ( query_field. get( "type" ) , Some ( & json!( [ "string" , "null" ] ) ) ) ;
1544+ assert_eq ! ( body_field. get( "type" ) , Some ( & json!( [ "string" , "null" ] ) ) ) ;
1545+
1546+ let mut api = build_api ( vec ! [ query_param( "optional" , query_field) ] , Some ( body_field) ) ;
1547+ let _ = TransformOpenApi :: new ( & mut api) . strip_null_from_query_params ( ) ;
1548+
1549+ // Query param should have null stripped
1550+ assert_eq ! (
1551+ get_param_schema( & api, 0 ) . get( "type" ) ,
1552+ Some ( & json!( "string" ) )
1553+ ) ;
1554+
1555+ // Request body schema should be unchanged
1556+ assert_eq ! (
1557+ get_body_schema( & api) . get( "type" ) ,
1558+ Some ( & json!( [ "string" , "null" ] ) ) ,
1559+ "request body schema should retain its nullable type"
1560+ ) ;
1561+ }
1562+ }
0 commit comments