Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/Swashbuckle.AspNetCore.SwaggerGen/JsonModelFactory.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
using System.Text.Json.Nodes;
using Microsoft.OpenApi;

namespace Swashbuckle.AspNetCore;

#nullable enable

internal static class JsonModelFactory
{
public static JsonNode CreateFromJson(string json)
=> json is null ? null : JsonNode.Parse(json);
public static JsonNode? CreateFromJson(string? json)
{
if (json is null)
return null;

if (json == "null")
return JsonNullSentinel.JsonNull;

return JsonNode.Parse(json);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ public static JsonNode Create(
string exampleString)
{
var type = schema?.ResolveType(schemaRepository);

var isStringType = type is { } value &&
value.HasFlag(JsonSchemaType.String) &&
!value.HasFlag(JsonSchemaType.Null);

if (isStringType)
if (type is { } schemaType)
{
return string.Equals(exampleString, "null") ? JsonNullSentinel.JsonNull : JsonValue.Create(exampleString);
var isStringType = schemaType.HasFlag(JsonSchemaType.String);
var nullable = schemaType.HasFlag(JsonSchemaType.Null);

if (isStringType)
{
return nullable && string.Equals(exampleString, "null")
? JsonNullSentinel.JsonNull
: JsonValue.Create(exampleString);
}
}

// HACK If the value is a string, but we can't detect it as one, then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,9 +802,9 @@
"required": true,
"schema": {
"type": "string",
"example": null
"example": "null"
Copy link
Contributor

Choose a reason for hiding this comment

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

For applications that do not have nullable enable this sounds weird

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is the weird to applications which do not have nullable enabled? When the generated schema does indicate, that null isn't allowed having null in the example is wrong. For strings to use than just the value the developer has put there in the example would be a single a good understandable solution.

Copy link
Contributor

@jgarciadelanoceda jgarciadelanoceda Feb 19, 2026

Choose a reason for hiding this comment

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

But an application which has nullable enabled the desired behaviour I think it'll be to present that value as null(There is no possibility to use string?).. Indeed in the SwashBuckle versions prior to 10 that was the behaviour.. What I tried to do in #3803 is to put the null when the type is null or when is a string that is not DateTime/DateOnly/TimeOnly/Timespan (Because those had the possibility always to have the DateTime?/DateOnly?...)

Also think about the possibility that the client is using OpenApi 3.0.. To put the example as null seems fine.. Also I think that if the client put null we must never put the nullstring.. Because the client mean null not "null"(Which was the original issue #3784)

Copy link
Contributor

Choose a reason for hiding this comment

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

If you want you can merge my changes (There are a couple of test more with DateTime/DateOnly).. And get to a consensus

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The library already handles the nullable type indication correctly. So when nullable is enabled

public string Prop { get; set; };
{
  "type": "string"
}

If nullable annotations are not enabled:

public string Prop { get; set; };
{
  "type": "string",
  "nullable": true
}

Also the difference between OpenApi 3.0 and 3.1 doesn't make any difference here as the OpenApi.NET library abstracts the difference away:

OpenApi v3.0

{
  "type": "string",
  "nullable": true
}

OpenApi v3.1

{
   "type": ["null","string"],
}

},
"example": null
"example": "null"
},
{
"name": "city",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,9 +802,9 @@
"required": true,
"schema": {
"type": "string",
"example": null
"example": "null"
},
"example": null
"example": "null"
},
{
"name": "city",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,9 +802,9 @@
"required": true,
"schema": {
"type": "string",
"example": null
"example": "null"
},
"example": null
"example": "null"
},
{
"name": "city",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public enum JsonConverterAnnotatedEnum
Value1 = 2,
Value2 = 4,
[EnumMember(Value = "X-foo")]
X = 8
X = 8,
[EnumMember(Value = "null")]
ValueNull
}
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ public void GenerateSchema_SetNullableFlag_IfInlineEnum()
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultValue), "2147483647")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), null)]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), "null")]
public void GenerateSchema_SetsDefault_IfPropertyHasDefaultValueAttribute(
Type declaringType,
string propertyName,
Expand Down Expand Up @@ -783,7 +783,7 @@ public void GenerateSchema_HonorsSerializerAttribute_StringEnumConverter()

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(JsonSchemaTypes.String, schema.Type);
Assert.Equal(["\"Value1\"", "\"Value2\"", "\"X-foo\""], schema.Enum.Select(openApiAny => openApiAny.ToJson()));
Assert.Equal(["\"Value1\"", "\"Value2\"", "\"X-foo\"", "\"null\""], schema.Enum.Select(openApiAny => openApiAny.ToJson()));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ namespace Swashbuckle.AspNetCore.SwaggerGen.Test;
/// <param name="StringProperty" example="Example for StringProperty">Summary for StringProperty</param>
/// <param name="StringPropertyWithUri" example="https://test.com/a?b=1&amp;c=2">Summary for StringPropertyWithUri</param>
/// <param name="ObjectProperty" example="{&quot;prop1&quot;: 1, &quot;prop2&quot;: &quot;foobar&quot;}">Summary for ObjectProperty</param>
/// <param name="ObjectPropertyWithNullExample" example="null">Summary for ObjectPropertyWithNullExample</param>
/// <param name="ArrayProperty" example="[&quot;value1&quot;, &quot;value2&quot;]">Summary for ArrayProperty</param>
/// <param name="ArrayPropertyWithNullExample" example="null">Summary for ArrayPropertyWithNullExample</param>
public record XmlAnnotatedRecord(
bool BoolProperty,
int IntProperty,
Expand All @@ -29,5 +32,8 @@ public record XmlAnnotatedRecord(
string StringPropertyWithNullExample,
string StringProperty,
string StringPropertyWithUri,
object ObjectProperty
object ObjectProperty,
object ObjectPropertyWithNullExample,
string[] ArrayProperty,
string[] ArrayPropertyWithNullExample
);
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ public class XmlAnnotatedType
/// <example>{"prop1": 1, "prop2": "foobar"}</example>
public object ObjectProperty { get; set; }

/// <summary>
/// Summary for ObjectPropertyWithNullExample
/// </summary>
/// <example>null</example>
public object ObjectPropertyWithNullExample { get; set; }

/// <summary>
/// Summary for ArrayProperty
/// </summary>
/// <example>["value1", "value2"]</example>
public string[] ArrayProperty { get; set; }


/// <summary>
/// Summary for ArrayPropertyWithNullExample
/// </summary>
/// <example>null</example>
public string[] ArrayPropertyWithNullExample { get; set; }

/// <summary>
/// Summary for AcceptsNothing
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType(
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringWithDefault), "\"foobar\"")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), null)]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultNullValue), "null")]
[InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.NullableIntWithDefaultValue), "2147483647")]
public void GenerateSchema_SetsDefault_IfPropertyHasDefaultValueAttribute(
Type declaringType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ public void Create_Returns_Null_When_Value_And_Schema_Are_Null()
}

[Fact]
public void Create_Returns_Null_When_Type_String_And_Value_Null_String_Literal()
public void Create_Returns_Null_When_Type_NullableString_And_Value_Null_String_Literal()
{
var schema = new OpenApiSchema { Type = JsonSchemaTypes.String };
var schema = new OpenApiSchema { Type = JsonSchemaTypes.String | JsonSchemaType.Null };
schemaRepository.AddDefinition("test", schema);

var example = XmlCommentsExampleHelper.Create(
Expand All @@ -84,6 +84,19 @@ public void Create_Returns_Null_When_Type_String_And_Value_Null_String_Literal()
Assert.Equal("null", example.ToJson());
}

[Fact]
public void Create_Returns_StringNull_When_Type_NotNullableString_And_Value_Null_String_Literal()
{
var schema = new OpenApiSchema { Type = JsonSchemaTypes.String };
schemaRepository.AddDefinition("test", schema);

var example = XmlCommentsExampleHelper.Create(
schemaRepository, schema, "null");

Assert.NotNull(example);
Assert.Equal("\"null\"", example.ToJson());
}

[Fact]
public void Create_Allows_Schema_To_Be_Null()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public void Apply_SetsDescription_FromPropertySummaryTag(
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.GuidProperty), JsonSchemaTypes.String, "\"d3966535-2637-48fa-b911-e3c27405ee09\"" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringProperty), JsonSchemaTypes.String, "\"Example for StringProperty\"" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ObjectProperty), JsonSchemaTypes.Object, "{\n \"prop1\": 1,\n \"prop2\": \"foobar\"\n}" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ArrayProperty), JsonSchemaTypes.Array, "[\n \"value1\",\n \"value2\"\n]" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.BoolProperty), JsonSchemaTypes.Boolean, "true" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.IntProperty), JsonSchemaTypes.Integer, "10" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.LongProperty), JsonSchemaTypes.Integer, "4294967295" },
Expand All @@ -74,10 +75,21 @@ public void Apply_SetsDescription_FromPropertySummaryTag(
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.GuidProperty), JsonSchemaTypes.String, "\"d3966535-2637-48fa-b911-e3c27405ee09\"" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringProperty), JsonSchemaTypes.String, "\"Example for StringProperty\"" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ObjectProperty), JsonSchemaTypes.Object, "{\n \"prop1\": 1,\n \"prop2\": \"foobar\"\n}" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ArrayProperty), JsonSchemaTypes.Array, "[\n \"value1\",\n \"value2\"\n]" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringPropertyWithUri), JsonSchemaTypes.String, "\"https://test.com/a?b=1\\u0026c=2\"" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringPropertyWithUri), JsonSchemaTypes.String, "\"https://test.com/a?b=1\\u0026c=2\"" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringPropertyWithNullExample), JsonSchemaTypes.String, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringPropertyWithNullExample), JsonSchemaTypes.String, "null" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringPropertyWithNullExample), JsonSchemaTypes.String, "\"null\"" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringPropertyWithNullExample), JsonSchemaTypes.String, "\"null\"" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringPropertyWithNullExample), JsonSchemaTypes.String | JsonSchemaTypes.Null, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringPropertyWithNullExample), JsonSchemaTypes.String | JsonSchemaTypes.Null, "null" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ObjectPropertyWithNullExample), JsonSchemaTypes.Object, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ObjectPropertyWithNullExample), JsonSchemaTypes.Object, "null" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ObjectPropertyWithNullExample), JsonSchemaTypes.Object | JsonSchemaTypes.Null, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ObjectPropertyWithNullExample), JsonSchemaTypes.Object | JsonSchemaTypes.Null, "null" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ArrayPropertyWithNullExample), JsonSchemaTypes.Array, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ArrayPropertyWithNullExample), JsonSchemaTypes.Array, "null" },
{ typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.ArrayPropertyWithNullExample), JsonSchemaTypes.Array | JsonSchemaTypes.Null, "null" },
{ typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.ArrayPropertyWithNullExample), JsonSchemaTypes.Array | JsonSchemaTypes.Null, "null" },
};

[Theory]
Expand Down