required string? incorrectly loses nullable and gains MinLength=1#1919
Conversation
Commit ec1f9c3 correctly wired up RequiredMemberAttribute (C# 11 required) to add properties to the required array in the schema. The mistake was folding it into the existing hasRequiredAttribute boolean, which is also used in both reflection services to suppress nullability and in JsonSchemaGenerator.cs:1216 to add MinLength=1 to strings. For required string?, propertyTypeDescription.IsNullable is true but hasRequiredAttribute is now also true (due to RequiredMemberAttribute), so isNullable becomes false. nullable:true is dropped and MinLength=1 is added. Both are wrong. The semantic distinction is: [Required] / [JsonRequired]: "this value must be non-null and non-empty" -- suppressing nullability and adding MinLength=1 are correct C# 11 required keyword: "this property must be present in object initializers / JSON deserialization, but the value can still be null" -- should only add to the required array; nullability and MinLength come from the type alone Fix: introduce hasSemanticRequiredAttribute (excludes RequiredMemberAttribute) for the isNullable and MinLength decisions in both reflection services. C# property | required array | nullable | minLength -----------------------------|----------------|----------|---------- string name | no | no | - string? name | no | yes | - [Required] string name | yes | no | 1 [Required] string? name | yes | no | 1 required string name | yes | no | - required string? name | yes | yes | - [JsonRequired] string? name | yes | no | - The Newtonsoft path has no hasJsonRequiredAttribute concept, so it only needs the requiredAttribute == null guard. The System.Text.Json path keeps [JsonRequired] in the semantic bucket since [JsonRequired] explicitly states the JSON value must not be null, unlike the language-level required keyword. Fixes RicoSuter#1918 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
7e4629d to
a3e51aa
Compare
|
Wondering whether this is also then correct in the context of OpenAPI? Ref: #1908 |
Sorry, I'm not sure why there's a timeout for The fix also holds in the context of OpenAPI, because in both JSON Schema and OpenAPI required and nullability are orthogonal concepts:
The C# The one place that's worth scrutinizing is |
[JsonRequired] in System.Text.Json indicates the JSON property must be present during deserialization, not that the value must be non-null. Treat it the same as the C# required keyword: add the property to the schema's required array, but preserve the declared nullability and do not add MinLength = 1 for strings. Only [Required] from DataAnnotations carries the 'non-null value required' semantics that suppress nullability and set MinLength = 1.
|
@inghamc @sychare @killergege @JohnGalt1717 @LeeLenaleee Full behavior comparisonTuple notation: (in required array / System.Text.Json path
Newtonsoft.Json path
Newtonsoft has no NSwag-generated TypeScript clientTemplate:
The NSwag-generated C# client (typed DTOs)NSwag's CSharp generator does not emit the
Exact emitted attributes depend on NSwag settings, but the nullability and required-array membership drive the shape. Summary of deltasvs pre-11.6.0 — restores nullability handling, preserves the #1908 feature ( vs v11.6.0 — two intentional behavior changes, both restoring correctness:
Swagger 2.0 addendumSwagger 2.0 mode shares the same schema-model code path, so the bug applied there too. Swagger 2.0 expresses nullability as the NJsonSchema-specific Swagger 2.0 serialized output —
NSwag reads the required array + v11.6.0 broke Swagger 2.0 the same way as JSON Schema / OpenAPI 3.x, and this PR fixes it via the same schema-model correction. Pre-11.6.0 behavior is not fully restored for Swagger 2.0: |
etc. required string of any type is not nullable, must be present, and "" is valid as is any other value of string. The same is true with the [Required] attribute. Everything else looks ok to me. |
|
I believe that matches what I'd be expecting for the generated TS client, and what specifically broke for us here. |
|
Thanks for the feedback @JohnGalt1717. The
For This has been NJsonSchema's behavior since well before v11.6.0 — there's a dedicated test ( Fine to merge and release? |
|
One would think that would be StringLength(1) on top of required, but ok. |
Fixes #1918.
Root cause
Commit ec1f9c3 correctly wired up
RequiredMemberAttribute(C# 11required) to add properties to therequiredarray in the schema. The mistake was folding it into the existinghasRequiredAttributeboolean, which is also used in both reflection services to suppress nullability and inJsonSchemaGenerator.cs:1216to addMinLength=1to strings.For
required string?,propertyTypeDescription.IsNullableistruebuthasRequiredAttributeis now alsotrue(due toRequiredMemberAttribute), soisNullablebecomesfalse.nullable:trueis dropped andMinLength=1is added. Both are wrong.The semantic distinction is:
[Required]/[JsonRequired]: "this value must be non-null and non-empty" -- suppressing nullability and addingMinLength=1are correctrequiredkeyword: "this property must be present in object initializers / JSON deserialization, but the value can still be null" -- should only add to therequiredarray; nullability andMinLengthcome from the type aloneFix
Introduce
hasSemanticRequiredAttribute(excludesRequiredMemberAttribute) for theisNullableandMinLengthdecisions in both reflection services.string namestring? name[Required] string name[Required] string? namerequired string namerequired string? name[JsonRequired] string? nameThe Newtonsoft path has no
hasJsonRequiredAttributeconcept, so it only needs therequiredAttribute == nullguard. The System.Text.Json path keeps[JsonRequired]in the semantic bucket since[JsonRequired]explicitly states the JSON value must not be null, unlike the language-levelrequiredkeyword.Tests
Regression tests added to
AttributeGenerationTests(Newtonsoft) andSystemTextJsonTests(System.Text.Json) asserting thatrequired string?lands inRequiredProperties, hasIsNullable = true, and hasMinLength = null.Follow-up (added by @RicoSuter)
Extended the fix to also treat
[JsonRequired]as a presence-only marker on the System.Text.Json path, for symmetry with the C#requiredkeyword and to match STJ's actual runtime semantics ([JsonRequired]enforces presence during deserialization, not value non-nullness).This means the
hasSemanticRequiredAttributeconcept collapses —[Required](DataAnnotations) is now the single signal that suppresses nullability and triggersMinLength = 1on strings, across both STJ and Newtonsoft paths.Updated truth table:
string namestring? name[Required] string name[Required] string? namerequired string namerequired string? name[JsonRequired] string name[JsonRequired] string? nameBehavior changes vs v11.6.0
required string?and[JsonRequired] string?are now correctlynullable: truewith noMinLength. Also fixes the regression reported in NSwag#5359.required stringand[JsonRequired] string(non-nullable types) no longer getMinLength = 1. Neither attribute says anything about string emptiness; users who want that should use[Required]or[MinLength(1)]explicitly.Pre-v11.6.0 nullability semantics are restored, and the
requiredkeyword /[JsonRequired]→ required-array feature added in #1908 is preserved.Added one regression test for
[JsonRequired] string?inSystemTextJsonTests.