@@ -21,7 +21,7 @@ namespace Microsoft.OpenApi
2121 /// </summary>
2222 public class OpenApiSchema : IOpenApiExtensible , IOpenApiSchema , IOpenApiSchemaMissingProperties , IOpenApiSchemaWithUnevaluatedProperties , IMetadataContainer
2323 {
24- private static readonly IEnumerable < JsonNode > s_singleNullElementList = [ JsonNullSentinel . JsonNull ] ;
24+ private static readonly IEnumerable < JsonNode > s_singleNullElementList = [ JsonNullSentinel . JsonNull ] ;
2525
2626 /// <inheritdoc />
2727 public string ? Title { get ; set ; }
@@ -545,7 +545,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
545545 }
546546
547547 // type
548- SerializeTypeProperty ( writer , version ) ;
548+ SerializeTypePropertyForVersion3AndLater ( writer , version , callback ) ;
549549
550550 // allOf
551551 writer . WriteOptionalCollection ( OpenApiConstants . AllOf , AllOf , callback ) ;
@@ -680,7 +680,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
680680 writer . WriteProperty ( OpenApiConstants . Id , Id ) ;
681681 writer . WriteProperty ( OpenApiConstants . DollarSchema , Schema ? . ToString ( ) ) ;
682682 writer . WriteProperty ( OpenApiConstants . Comment , Comment ) ;
683-
683+
684684 if ( WasConstExplicitlySet )
685685 {
686686 writer . WriteRequiredProperty ( OpenApiConstants . Const , Const ) ;
@@ -691,7 +691,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
691691 writer . WriteProperty ( OpenApiConstants . Anchor , Anchor ) ;
692692 writer . WriteProperty ( OpenApiConstants . DynamicRef , DynamicRef ) ;
693693 writer . WriteProperty ( OpenApiConstants . DynamicAnchor , DynamicAnchor ) ;
694-
694+
695695 // UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
696696 // Only emit when the type could include objects.
697697 // Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
@@ -833,7 +833,7 @@ private void SerializeAsV2(
833833 writer . WriteStartObject ( ) ;
834834
835835 // type
836- SerializeTypeProperty ( writer , OpenApiSpecVersion . OpenApi2_0 ) ;
836+ SerializeTypePropertyForVersion2 ( writer ) ;
837837
838838 // description
839839 writer . WriteProperty ( OpenApiConstants . Description , Description ) ;
@@ -915,7 +915,7 @@ private void SerializeAsV2(
915915 // oneOf (Not Supported in V2) - Write the first schema only as an allOf.
916916 writer . WriteOptionalCollection ( OpenApiConstants . AllOf , OneOf ? . Take ( 1 ) , ( w , s ) => s . SerializeAsV2 ( w ) ) ;
917917 }
918- #pragma warning restore CS0618
918+ #pragma warning restore CS0618
919919 }
920920
921921 // properties
@@ -1001,31 +1001,104 @@ private void SerializeAsV2(
10011001 writer . WriteEndObject ( ) ;
10021002 }
10031003
1004- private void SerializeTypeProperty ( IOpenApiWriter writer , OpenApiSpecVersion version )
1004+ private void SerializeTypePropertyForVersion2 ( IOpenApiWriter writer )
10051005 {
1006- var typeToUse = Type ;
1006+ if ( Type is not { } type || type == JsonSchemaType . Null )
1007+ {
1008+ return ;
1009+ }
1010+
1011+ var typeWithoutNull = type & ~ JsonSchemaType . Null ;
1012+ if ( ! HasMultipleTypes ( typeWithoutNull ) )
1013+ {
1014+ writer . WriteProperty ( OpenApiConstants . Type , typeWithoutNull . ToFirstIdentifier ( ) ) ;
1015+ }
1016+ }
10071017
1008- if ( typeToUse is null )
1018+ /// <summary>
1019+ /// Serializes the "type" property for OpenAPI v3 and later versions,
1020+ /// falling back to anyOf/oneOf when multiple types cannot be expressed
1021+ /// using the "type" property alone (OpenAPI 3.0).
1022+ /// </summary>
1023+ private void SerializeTypePropertyForVersion3AndLater ( IOpenApiWriter writer , OpenApiSpecVersion version , Action < IOpenApiWriter , IOpenApiSerializable > callback )
1024+ {
1025+ if ( Type is not { } type )
10091026 {
10101027 return ;
10111028 }
10121029
1013- switch ( version )
1030+ if ( version == OpenApiSpecVersion . OpenApi3_0 )
10141031 {
1015- case OpenApiSpecVersion . OpenApi2_0 or OpenApiSpecVersion . OpenApi3_0 :
1016- var typeWithoutNull = typeToUse . Value & ~ JsonSchemaType . Null ;
1017- if ( typeWithoutNull != 0 && ! HasMultipleTypes ( typeWithoutNull ) )
1032+ if ( type == JsonSchemaType . Null )
1033+ {
1034+ return ;
1035+ }
1036+
1037+ var typeWithoutNull = type & ~ JsonSchemaType . Null ;
1038+ var hasNull = typeWithoutNull != type ;
1039+ var arrayWithoutNull = ( from JsonSchemaType flag in jsonSchemaTypeValues
1040+ where typeWithoutNull . HasFlag ( flag )
1041+ select flag ) . ToArray ( ) ;
1042+
1043+ // - If we have more than one type (excluding null), we have to use anyOf/oneOf.
1044+ // - If we have exactly one type alone (without null), we emit the type property.
1045+ // - If we have exactly one non-null type and also we have the null type, we emit the type property and nullable: true (handled in SerializeNullable)
1046+ if ( arrayWithoutNull . Length > 1 )
1047+ {
1048+ // If the schema doesn't already have anyOf/oneOf, we can write multiple types as such.
1049+ var canWriteAsAnyOf = AnyOf is not { Count : > 0 } ;
1050+ var canWriteAsOneOf = OneOf is not { Count : > 0 } ;
1051+ if ( canWriteAsAnyOf )
10181052 {
1019- writer . WriteProperty ( OpenApiConstants . Type , typeWithoutNull . ToFirstIdentifier ( ) ) ;
1053+ writer . WriteOptionalCollection ( OpenApiConstants . AnyOf , ConstructChildSchemasForTypes ( arrayWithoutNull , hasNull ) , callback ) ;
10201054 return ;
10211055 }
1022- break ;
1023- default :
1024- WriteUnifiedSchemaType ( typeToUse . Value , writer ) ;
1056+ else if ( canWriteAsOneOf )
1057+ {
1058+ writer . WriteOptionalCollection ( OpenApiConstants . OneOf , ConstructChildSchemasForTypes ( arrayWithoutNull , hasNull ) , callback ) ;
1059+ return ;
1060+ }
1061+ }
1062+ else if ( arrayWithoutNull . Length == 1 )
1063+ {
1064+ writer . WriteProperty ( OpenApiConstants . Type , arrayWithoutNull [ 0 ] . ToSingleIdentifier ( ) ) ;
10251065 return ;
1066+ }
1067+ }
1068+ else
1069+ {
1070+ var array = ( from JsonSchemaType flag in jsonSchemaTypeValues
1071+ where type . HasFlag ( flag )
1072+ select flag ) . ToArray ( ) ;
1073+
1074+ if ( array . Length > 1 )
1075+ {
1076+ writer . WriteOptionalCollection ( OpenApiConstants . Type , array , ( w , s ) => w . WriteValue ( s . ToSingleIdentifier ( ) ) ) ;
1077+ }
1078+ else if ( array . Length == 1 )
1079+ {
1080+ writer . WriteProperty ( OpenApiConstants . Type , array [ 0 ] . ToSingleIdentifier ( ) ) ;
1081+ }
10261082 }
10271083
1028- return ;
1084+ static OpenApiSchema [ ] ConstructChildSchemasForTypes ( JsonSchemaType [ ] types , bool hasNull )
1085+ {
1086+ var schemas = new OpenApiSchema [ types . Length + ( hasNull ? 1 : 0 ) ] ;
1087+ for ( int i = 0 ; i < types . Length ; i ++ )
1088+ {
1089+ schemas [ i ] = new OpenApiSchema ( )
1090+ {
1091+ Type = types [ i ]
1092+ } ;
1093+ }
1094+
1095+ if ( hasNull )
1096+ {
1097+ schemas [ schemas . Length - 1 ] = new OpenApiSchema ( ) { Type = JsonSchemaType . Null } ;
1098+ }
1099+
1100+ return schemas ;
1101+ }
10291102 }
10301103
10311104 private JsonNode ? GetCompatibilityExample ( )
@@ -1063,27 +1136,6 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType)
10631136 return ! IsPowerOfTwo ( schemaTypeNumeric ) ;
10641137 }
10651138
1066- private static void WriteUnifiedSchemaType ( JsonSchemaType type , IOpenApiWriter writer )
1067- {
1068- var array = ( from JsonSchemaType flag in jsonSchemaTypeValues
1069- where type . HasFlag ( flag )
1070- select flag . ToFirstIdentifier ( ) ) . ToArray ( ) ;
1071- if ( array . Length > 1 )
1072- {
1073- writer . WriteOptionalCollection ( OpenApiConstants . Type , array , ( w , s ) =>
1074- {
1075- if ( ! string . IsNullOrEmpty ( s ) && s is not null )
1076- {
1077- w . WriteValue ( s ) ;
1078- }
1079- } ) ;
1080- }
1081- else
1082- {
1083- writer . WriteProperty ( OpenApiConstants . Type , array [ 0 ] ) ;
1084- }
1085- }
1086-
10871139 private void SerializeNullable ( IOpenApiWriter writer , OpenApiSpecVersion version )
10881140 {
10891141 if ( HasNullType )
0 commit comments