Skip to content

Commit 40d84b1

Browse files
[release/10.0] Fix openapi schema type null for UrlAttribute and Base64StringAttribute (#63528)
* Fix openapi schema null type for UrlAttribute and Base64StringAttribute * Fix copilot nits --------- Co-authored-by: Sjoerd van der Meer <[email protected]>
1 parent 7d73258 commit 40d84b1

File tree

2 files changed

+297
-2
lines changed

2 files changed

+297
-2
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
8585
{
8686
if (attribute is Base64StringAttribute)
8787
{
88-
schema[OpenApiSchemaKeywords.TypeKeyword] = JsonSchemaType.String.ToString();
8988
schema[OpenApiSchemaKeywords.FormatKeyword] = "byte";
9089
}
9190
else if (attribute is RangeAttribute rangeAttribute)
@@ -153,7 +152,6 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
153152
}
154153
else if (attribute is UrlAttribute)
155154
{
156-
schema[OpenApiSchemaKeywords.TypeKeyword] = JsonSchemaType.String.ToString();
157155
schema[OpenApiSchemaKeywords.FormatKeyword] = "uri";
158156
}
159157
else if (attribute is StringLengthAttribute stringLengthAttribute)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.PropertySchemas.cs

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,236 @@ await VerifyOpenApiDocument(builder, document =>
411411
});
412412
}
413413

414+
[Fact]
415+
public async Task GetOpenApiSchema_Base64StringAttribute_StringProperties()
416+
{
417+
var builder = CreateBuilder();
418+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
419+
420+
await VerifyOpenApiDocument(builder, document =>
421+
{
422+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
423+
var nonNullable = schema.Properties["base64StringValue"];
424+
var nullable = schema.Properties["nullableBase64StringValue"];
425+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
426+
Assert.Equal("byte", nonNullable.Format);
427+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
428+
Assert.Equal("byte", nullable.Format);
429+
});
430+
}
431+
432+
[Fact]
433+
public async Task GetOpenApiSchema_RangeAttribute_IntProperties()
434+
{
435+
var builder = CreateBuilder();
436+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
437+
438+
await VerifyOpenApiDocument(builder, document =>
439+
{
440+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
441+
var nonNullable = schema.Properties["rangeIntValue"];
442+
var nullable = schema.Properties["nullableRangeIntValue"];
443+
Assert.Equal(JsonSchemaType.Integer, nonNullable.Type);
444+
Assert.Equal("int32", nonNullable.Format);
445+
Assert.Equal("1", nonNullable.Minimum);
446+
Assert.Equal("100", nonNullable.Maximum);
447+
Assert.Equal(JsonSchemaType.Integer | JsonSchemaType.Null, nullable.Type);
448+
Assert.Equal("int32", nullable.Format);
449+
Assert.Equal("1", nullable.Minimum);
450+
Assert.Equal("100", nullable.Maximum);
451+
});
452+
}
453+
454+
[Fact]
455+
public async Task GetOpenApiSchema_RangeAttribute_DoubleProperties()
456+
{
457+
var builder = CreateBuilder();
458+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
459+
460+
await VerifyOpenApiDocument(builder, document =>
461+
{
462+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
463+
var nonNullable = schema.Properties["rangeDoubleValue"];
464+
var nullable = schema.Properties["nullableRangeDoubleValue"];
465+
Assert.Equal(JsonSchemaType.Number, nonNullable.Type);
466+
Assert.Equal("double", nonNullable.Format);
467+
Assert.Equal("0.1", nonNullable.Minimum as string);
468+
Assert.Equal("99.9", nonNullable.Maximum as string);
469+
Assert.Equal(JsonSchemaType.Number | JsonSchemaType.Null, nullable.Type);
470+
Assert.Equal("double", nullable.Format);
471+
Assert.Equal("0.1", nullable.Minimum as string);
472+
Assert.Equal("99.9", nullable.Maximum as string);
473+
});
474+
}
475+
476+
[Fact]
477+
public async Task GetOpenApiSchema_RegularExpressionAttribute_StringProperties()
478+
{
479+
var builder = CreateBuilder();
480+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
481+
482+
await VerifyOpenApiDocument(builder, document =>
483+
{
484+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
485+
var nonNullable = schema.Properties["regexStringValue"];
486+
var nullable = schema.Properties["nullableRegexStringValue"];
487+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
488+
Assert.Equal("^[A-Z]{3}$", nonNullable.Pattern);
489+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
490+
Assert.Equal("^[A-Z]{3}$", nullable.Pattern);
491+
});
492+
}
493+
494+
[Fact]
495+
public async Task GetOpenApiSchema_MaxLengthAttribute_StringProperties()
496+
{
497+
var builder = CreateBuilder();
498+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
499+
500+
await VerifyOpenApiDocument(builder, document =>
501+
{
502+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
503+
var nonNullable = schema.Properties["maxLengthStringValue"];
504+
var nullable = schema.Properties["nullableMaxLengthStringValue"];
505+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
506+
Assert.Equal(10, nonNullable.MaxLength);
507+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
508+
Assert.Equal(10, nullable.MaxLength);
509+
});
510+
}
511+
512+
[Fact]
513+
public async Task GetOpenApiSchema_MaxLengthAttribute_ArrayProperties()
514+
{
515+
var builder = CreateBuilder();
516+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
517+
518+
await VerifyOpenApiDocument(builder, document =>
519+
{
520+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
521+
var nonNullable = schema.Properties["maxLengthArrayValue"];
522+
var nullable = schema.Properties["nullableMaxLengthArrayValue"];
523+
Assert.Equal(JsonSchemaType.Array, nonNullable.Type);
524+
Assert.Equal(5, nonNullable.MaxItems);
525+
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, nullable.Type);
526+
Assert.Equal(5, nullable.MaxItems);
527+
});
528+
}
529+
530+
[Fact]
531+
public async Task GetOpenApiSchema_MinLengthAttribute_StringProperties()
532+
{
533+
var builder = CreateBuilder();
534+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
535+
536+
await VerifyOpenApiDocument(builder, document =>
537+
{
538+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
539+
var nonNullable = schema.Properties["minLengthStringValue"];
540+
var nullable = schema.Properties["nullableMinLengthStringValue"];
541+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
542+
Assert.Equal(3, nonNullable.MinLength);
543+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
544+
Assert.Equal(3, nullable.MinLength);
545+
});
546+
}
547+
548+
[Fact]
549+
public async Task GetOpenApiSchema_MinLengthAttribute_ArrayProperties()
550+
{
551+
var builder = CreateBuilder();
552+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
553+
554+
await VerifyOpenApiDocument(builder, document =>
555+
{
556+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
557+
var nonNullable = schema.Properties["minLengthArrayValue"];
558+
var nullable = schema.Properties["nullableMinLengthArrayValue"];
559+
Assert.Equal(JsonSchemaType.Array, nonNullable.Type);
560+
Assert.Equal(2, nonNullable.MinItems);
561+
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, nullable.Type);
562+
Assert.Equal(2, nullable.MinItems);
563+
});
564+
}
565+
566+
[Fact]
567+
public async Task GetOpenApiSchema_LengthAttribute_StringProperties()
568+
{
569+
var builder = CreateBuilder();
570+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
571+
572+
await VerifyOpenApiDocument(builder, document =>
573+
{
574+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
575+
var nonNullable = schema.Properties["lengthStringValue"];
576+
var nullable = schema.Properties["nullableLengthStringValue"];
577+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
578+
Assert.Equal(2, nonNullable.MinLength);
579+
Assert.Equal(8, nonNullable.MaxLength);
580+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
581+
Assert.Equal(2, nullable.MinLength);
582+
Assert.Equal(8, nullable.MaxLength);
583+
});
584+
}
585+
586+
[Fact]
587+
public async Task GetOpenApiSchema_LengthAttribute_ArrayProperties()
588+
{
589+
var builder = CreateBuilder();
590+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
591+
592+
await VerifyOpenApiDocument(builder, document =>
593+
{
594+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
595+
var nonNullable = schema.Properties["lengthArrayValue"];
596+
var nullable = schema.Properties["nullableLengthArrayValue"];
597+
Assert.Equal(JsonSchemaType.Array, nonNullable.Type);
598+
Assert.Equal(1, nonNullable.MinItems);
599+
Assert.Equal(4, nonNullable.MaxItems);
600+
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, nullable.Type);
601+
Assert.Equal(1, nullable.MinItems);
602+
Assert.Equal(4, nullable.MaxItems);
603+
});
604+
}
605+
606+
[Fact]
607+
public async Task GetOpenApiSchema_UrlAttribute_StringProperties()
608+
{
609+
var builder = CreateBuilder();
610+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
611+
612+
await VerifyOpenApiDocument(builder, document =>
613+
{
614+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
615+
var nonNullable = schema.Properties["urlStringValue"];
616+
var nullable = schema.Properties["nullableUrlStringValue"];
617+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
618+
Assert.Equal("uri", nonNullable.Format);
619+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
620+
Assert.Equal("uri", nullable.Format);
621+
});
622+
}
623+
624+
[Fact]
625+
public async Task GetOpenApiSchema_StringLengthAttribute_StringProperties()
626+
{
627+
var builder = CreateBuilder();
628+
builder.MapPost("/api", (PropertiesWithDataAnnotations model) => { });
629+
630+
await VerifyOpenApiDocument(builder, document =>
631+
{
632+
var schema = document.Paths["/api"].Operations[HttpMethod.Post].RequestBody.Content.First().Value.Schema;
633+
var nonNullable = schema.Properties["stringLengthValue"];
634+
var nullable = schema.Properties["nullableStringLengthValue"];
635+
Assert.Equal(JsonSchemaType.String, nonNullable.Type);
636+
Assert.Equal(5, nonNullable.MinLength);
637+
Assert.Equal(20, nonNullable.MaxLength);
638+
Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, nullable.Type);
639+
Assert.Equal(5, nullable.MinLength);
640+
Assert.Equal(20, nullable.MaxLength);
641+
});
642+
}
643+
414644
#nullable enable
415645
private class NullablePropertiesTestModel
416646
{
@@ -452,5 +682,72 @@ private class NullablePropertiesWithValidationModel
452682
[Description("A description field")]
453683
public string? NullableDescription { get; set; }
454684
}
685+
686+
private class PropertiesWithDataAnnotations
687+
{
688+
// Base64StringAttribute
689+
[Base64String]
690+
public string Base64StringValue { get; set; } = string.Empty;
691+
[Base64String]
692+
public string? NullableBase64StringValue { get; set; }
693+
694+
// RangeAttribute
695+
[Range(1, 100)]
696+
public int RangeIntValue { get; set; } = 0;
697+
[Range(1, 100)]
698+
public int? NullableRangeIntValue { get; set; }
699+
[Range(0.1, 99.9)]
700+
public double RangeDoubleValue { get; set; } = 0.0;
701+
[Range(0.1, 99.9)]
702+
public double? NullableRangeDoubleValue { get; set; }
703+
704+
// RegularExpressionAttribute
705+
[RegularExpression(@"^[A-Z]{3}$")]
706+
public string RegexStringValue { get; set; } = string.Empty;
707+
[RegularExpression(@"^[A-Z]{3}$")]
708+
public string? NullableRegexStringValue { get; set; }
709+
710+
// MaxLengthAttribute
711+
[MaxLength(10)]
712+
public string MaxLengthStringValue { get; set; } = string.Empty;
713+
[MaxLength(10)]
714+
public string? NullableMaxLengthStringValue { get; set; }
715+
[MaxLength(5)]
716+
public int[] MaxLengthArrayValue { get; set; } = [];
717+
[MaxLength(5)]
718+
public int[]? NullableMaxLengthArrayValue { get; set; }
719+
720+
// MinLengthAttribute
721+
[MinLength(3)]
722+
public string MinLengthStringValue { get; set; } = string.Empty;
723+
[MinLength(3)]
724+
public string? NullableMinLengthStringValue { get; set; }
725+
[MinLength(2)]
726+
public int[] MinLengthArrayValue { get; set; } = [];
727+
[MinLength(2)]
728+
public int[]? NullableMinLengthArrayValue { get; set; }
729+
730+
// LengthAttribute (custom, if available)
731+
[Length(2, 8)]
732+
public string LengthStringValue { get; set; } = string.Empty;
733+
[Length(2, 8)]
734+
public string? NullableLengthStringValue { get; set; }
735+
[Length(1, 4)]
736+
public int[] LengthArrayValue { get; set; } = [];
737+
[Length(1, 4)]
738+
public int[]? NullableLengthArrayValue { get; set; }
739+
740+
// UrlAttribute
741+
[Url]
742+
public string UrlStringValue { get; set; } = string.Empty;
743+
[Url]
744+
public string? NullableUrlStringValue { get; set; }
745+
746+
// StringLengthAttribute
747+
[StringLength(20, MinimumLength = 5)]
748+
public string StringLengthValue { get; set; } = string.Empty;
749+
[StringLength(20, MinimumLength = 5)]
750+
public string? NullableStringLengthValue { get; set; }
751+
}
455752
#nullable restore
456753
}

0 commit comments

Comments
 (0)