Skip to content

Do not ignore multiple types when serializing to 3.0 - #2960

Open
Youssef1313 wants to merge 2 commits into
microsoft:mainfrom
Youssef1313:dev/ygerges/multiple-types-3
Open

Do not ignore multiple types when serializing to 3.0#2960
Youssef1313 wants to merge 2 commits into
microsoft:mainfrom
Youssef1313:dev/ygerges/multiple-types-3

Conversation

@Youssef1313

@Youssef1313 Youssef1313 commented Jul 15, 2026

Copy link
Copy Markdown
Member

Fixes #2939

  • The change to ToSingleIdentifier is only a simplification to make it easier to read.
  • Type serialization is now split to two path to be easier to read, especially that 3.0 needs more parameters/info than 2.0.
  • The behavior of 2.0 is maintained as-is, no change there
  • Previously, in 3.0, we ignored when Type had multiple values (e.g, String and Integer). This is now handled using oneOf or anyOf (whichever isn't used by the current schema already). If both are used, we ignore as it we used to in the past.
  • The general idea here is:
    1. We take the Type.
    2. We remove Null from it (if it exists).
    3. If, after removing null, we have a single type, we simply emit it, and conditionally emit nullable: true depending on whether null was existing. This is the same as the existing behavior.
    4. If, after removing null, we have more than one type, we create a oneOf/anyOf with children schemas handling each type individually.
  • The reason around all that is that because type only supports a single value when using OpenApi 3.0.

For context: https://spec.openapis.org/oas/v3.0.4.html#json-schema-keywords

The following keywords are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification.
type - Value MUST be a string. Multiple types via an array are not supported.

Comment on lines +1974 to +1977
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer,
};

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, this was resulting in an empty schema, which will match anything.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, the type was ignored, and only anyOf was emitted.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, the type was ignored and only anyOf was emitted.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, the type was ignored and only oneOf was emitted.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, only oneOf was emitted.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my change, an empty schema was generated, allowing anything to match.

@Youssef1313
Youssef1313 force-pushed the dev/ygerges/multiple-types-3 branch from 165e0eb to a663f6f Compare July 15, 2026 11:31
{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, the behavior is the same as in the past. We are not respecting Type, and I can't see an easy way to respect it.

It's technically possible to emit "allOf" that contains a schema representing the types as "oneOf" or "anyOf" schema, and then another schema that wraps the existing anyOf/oneOf. But that's a major re-write I didn't want to introduce.

{
var schema = new OpenApiSchema()
{
Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. This wasn't (and is still not) respected. We could respect it but it will be a more bigger re-write.

@Youssef1313
Youssef1313 marked this pull request as ready for review July 15, 2026 11:36
@Youssef1313
Youssef1313 requested a review from a team as a code owner July 15, 2026 11:36

@baywet baywet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this kind of normalization should not be done during serialization, but rather through a walker which would be called before serialization.

See #808 and other related issues.

Case in point, we've had to revert some of the normalization done for null/nullable (to oneOf) in #2933

Copilot AI review requested due to automatic review settings July 30, 2026 07:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request updates OpenApiSchema JSON serialization to properly represent schemas with multiple JsonSchemaType flags when targeting OpenAPI 3.0, emitting anyOf/oneOf (when possible) instead of silently ignoring additional types, and adds coverage to validate the new behavior.

Changes:

  • Split type serialization logic between OpenAPI 2.0 and OpenAPI 3.0+ paths, adding 3.0-specific handling for multi-type schemas via anyOf/oneOf.
  • Add comprehensive unit tests covering multi-type combinations (with/without null, and with existing oneOf/anyOf present).
  • Simplify ToSingleIdentifier by using a lookup and providing a clearer exception for unexpected values.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs Adds tests validating multi-type serialization behavior for OpenAPI 3.0 across multiple composition scenarios.
src/Microsoft.OpenApi/Models/OpenApiSchema.cs Refactors and extends type serialization to support multi-type handling in 3.0 using anyOf/oneOf, keeping 2.0 behavior unchanged.
src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Updates ToSingleIdentifier to use a lookup and throw a clearer exception for unexpected inputs.
Comments suppressed due to low confidence (1)

src/Microsoft.OpenApi/Models/OpenApiSchema.cs:1091

  • In the v3.1+ path, TrySerializeTypePropertyForVersion3AndLater also assumes Enum.GetValues() will match at least one flag in the provided Type. If Type is 0 or contains only unknown bits, array will be empty and array[0] will throw IndexOutOfRangeException. Add an empty-array guard and return false when no valid flags are present.
                var array = (from JsonSchemaType flag in jsonSchemaTypeValues
                                        where type.HasFlag(flag)
                                        select flag).ToArray();

                if (array.Length > 1)
                {
                    writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) => w.WriteValue(s.ToSingleIdentifier()));
                }
                else
                {
                    writer.WriteProperty(OpenApiConstants.Type, array[0].ToSingleIdentifier());
                }

Comment thread src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@Youssef1313

Copy link
Copy Markdown
Member Author

I believe this kind of normalization should not be done during serialization, but rather through a walker which would be called before serialization.

Given the investigations I did for #2967, I think it's fine for us to continue ensuring, as much as possible, that we produce semantically equivalent document for all versions. The cases presented in #2967 fall into two groups:

  • user bug (e.g, the Swashbuckle bug, or the JsonApiDotNetCore bug)
  • deserialization bugs on our side, which we can simply fix.

I don't believe there is a need for a new public API to do the semantic transformations, and I don't think all transformations we might want to do in future will be representable in the object model. So we will end up with logic scattered between dedicated walkers and in the serialization itself. We shouldn't need to pay additional performance cost for deep cloning the whole document as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider representing multiple types in 3.0 using anyOf or oneOf

3 participants