-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fail null deserialization on non-nullable reference types #1256
Comments
I fully agree. Also for consistency shouldn't the absence of a non-nullable also cause a fail? (If we don't have something like this the whole advantage of nullable gets significantly negated as runtime types can deviate from compile-time checks. We're then back to manual fixes (run-time checks, custom converters or even attributes - all subject to human error and forgotten updates etc...).) |
I believe "nullable reference type" is a compile-time/static analysis feature. I don't think we can leverage it to affect runtime behavior of APIs like the The nullable annotation on the runtime/src/libraries/System.Text.Json/ref/System.Text.Json.cs Lines 439 to 440 in 3a844ad
What enabling the nullability will do is the compiler will warn you if you try to deserialize into a non-nullable reference type like your class. cc @safern, @stephentoub, @jcouv
Aside from implementing your own cc @steveharter, @layomia |
Yes I also agree that the nullable annotation for So that's why the compiler will warn you, this method my return null and you're using a non-nullable type. So what you got to do is declare your generic parameter and variable type as nullable. MyDataObject? invalidConverted = JsonSerializer.Deserialize<MyDataObject?>(invalidInput); |
@ahsonkhan EF Core is actually doing this, when you have NRTs enabled, they'll make columns |
So @ahsonkhan if I understand correctly you are suggesting a property attribute could be used to ensure NULL is ignored when de-serialising a non-nullable reference type. But don't we want a way to trigger an exception to be equivalent to the non-nullable value type case? And if we have an attribute or way to trigger an exception with NULL would that not also solve the compiler warning issue mentioned by @safern in that the compiler flow analysis can see that an incoming NULL will lead to an exception and hence the conversion to non-nullable is safe? I think it would be really great to have a solution that is compatible with the compile-time checker if possible. 😀 |
A couple of additional thoughts meant fully in the interests of making C# and .NET Core the best development environment possible.
If the NRT complier flow analysis is not detecting things correctly, is it worth raising this with the compiler team? For example if the complier flow analysis is not smart about attributes shouldn't this be fixed?
I realise it may take work from multiple teams but shouldn't we aim for a better solution than disabling the complier flow analysis ( |
Any chance a fix for this is coming with .net 5? |
|
|
Would this help? "Expose top-level nullability information from reflection" (#29723) is slated for .NET 6 Preview 7. |
I might be missing something, but to me it may be more helpful to provide a way to define a fallback value for these types of fields. Imagine you create a model that you want to persist the serialized version of like so:
You serialize it and persist it, then go on to add another fields in an update:
If you attempt to deserialize the JSON that was generated in a prior iteration of the class, it seems a little harsh to get exceptions and could break backwards compatibility. Instead, wouldn't it make sense to just be able to fallback the
But at that point, any If there is a feature that allows me to do this already, please let me know! I really appreciate it. |
How about checking for Example: class Test
{
public string WithoutAttribute { get; set; }
public string? WithAttribute { get; set; }
public int PureValue { get; set; }
public int? ValueWrappedInNullable { get; set; }
} So those are 4 cases that needs to be considered like follows:
I'm not seeing why this can not be implemented without any new attribute, interface, or similar stuff. Let me know if I'm missing something. |
This is currently possible to achieve with contract customization except for top level (we don't have info on nullability for top level - for top level you can do null check separately). Here is example implementation: using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { BlockNullForNonNullableReferenceTypesModifier }
}
};
string json = """
{
"NonNullableString": null,
"NullableString": null
}
""";
TestClass testClass = JsonSerializer.Deserialize<TestClass>(json, options); // JsonException: Null value not allowed for non-nullable property.
static void BlockNullForNonNullableReferenceTypesModifier(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Type.IsValueType)
return;
NullabilityInfoContext context = new();
foreach (JsonPropertyInfo property in jsonTypeInfo.Properties)
{
Action<object, object?>? setter = property.Set;
if (setter == null)
continue;
NullabilityInfo? nullabilityInfo = null;
switch (property.AttributeProvider)
{
case FieldInfo fieldInfo:
nullabilityInfo = context.Create(fieldInfo);
break;
case PropertyInfo propertyInfo:
nullabilityInfo = context.Create(propertyInfo);
break;
}
if (nullabilityInfo == null)
continue;
if (nullabilityInfo.WriteState == NullabilityState.NotNull)
{
property.Set = (obj, val) =>
{
if (val == null)
throw new JsonException("Null value not allowed for non-nullable property.");
setter(obj, val);
};
}
}
}
class TestClass
{
public string NonNullableString { get; set; }
public string? NullableString { get; set; }
} there is extra work needed to cover all corner cases (array values etc) but this should hopefully cover most of the scenarios |
This has been open for years, and many related requests are also closed as duplicates, pointing to this issue. We are also facing this issue. What is the actual problem here to provide a solution for this? If nullable annottations are turned on, fail/warn (potentially based on a JsonSerializer setting/annotation), when null is received as the value of the attribute in Json. Currently it only fails when the attribute is not present at all (when I mark the property as required). |
@Hottemax biggest problem is that (de)serializers in general have virtually infinite number of knobs and expectations how people want it to work. We have significant number of open issues and requests and we try to make sure we look at them holistically rather than per single request/issue. We cannot just remove APIs once added so we need to be conservative in that choice. This issue has accumulated enough upvotes for it to be considered for next release but note that it doesn't guarantee we will do it. Soon we will look at all issues in 9.0 and Future milestone and make some initial choices but keep in mind that it's overall almost 200 issues (12 being already considered for 9.0). There is a huge difference in how much time it takes to write simple prototype code like one above vs actual code which ships - latter requires much more time for testing and convincing people on the right design. We need to pick which of these issues are currently most important for product as a whole (i.e. our main scenario is seamless interaction with ASP.NET Core which might be invisible for most of the users) but upvoted issues like this one are also a big factor for us when picking. Certainly having someone from community doing some prototyping would help here. I.e. extending code above to figure out all corner cases, some test cases etc would decrease amount of work for us and make it more likely it would ship next release. |
Thanks for the detailed provided context @krwq ! 👍 Replies like these are so valuable, so us library users know what is going on behind the scenes. |
Here's a prototype showing how NRT metadata can be extracted in both reflection and source gen: eiriktsarpalis/PolyType@e96ef20 One thing to note is that this will most likely require an opt-in switch -- it would otherwise be a breaking change: namespace System.Text.Json;
public partial class JsonSerializerOptions
{
public bool EnforceNonNullableReferenceTypes { get; set; }
}
public partial class JsonSourceGenerationOptionsAttribute
{
public bool EnforceNonNullableReferenceTypes { get; set; }
} |
One open question is whether the feature will be supported in the netfx/netstandard2.0 TFMs. Even though officially speaking NRTs are not supported in older TFMs, it still is possible to use them there. Supporting the feature would require OOBing the @dotnet/area-system-reflection @stephentoub @ericstj thoughts? |
FWIW, I don't think this is a desirable change. NRTs have never affected runtime behavior; in fact it's been a key aspect of the design that they're a compile-time-only assistance to provide warnings. Changing deserialization behavior based on these annotations goes against that. |
They do affect runtime behavior for ASP.NET Core model validation. |
They also affect how Entity Framework creates columns - an NRT causes a column to be nullable. This "key aspect" is so bad that not even Microsoft's products stick to it. Kotlin handles the "Billion Dollar Mistake" way better, despite interoperability with the Java ecosystem. |
NRTs convey intent as such it seems reasonable to me that automatic serializers generate schemas honoring that intent. |
I don't believe it's any different from:
That will result in a compile-time warning if |
I understand, but at the same time a type system is not the same thing as a serialization contract. In automatic serializers we use convention to map type system features to serialization contracts in a way that feels intuitive to the user. One good example is the Mapping non-nullable annotations to a "require non-null" deserialization contract seems useful to me for a couple of reasons:
I should clarify that the scope of this feature is limited to member and constructor parameter annotations. We can't add this to generic parameters/collection elements due to constraints in their runtime representation, at least in the reflection serializer. |
I think it is different because one of the most useful purposes of both reflection and source generators is to save the developer from writing a whole bunch of 'handwritten' code. It is from this perspective this feature request is thought about I believe. Taking your example above, your point is (I think) that the compiler warning helps the author remember to insert a null check. In code generation, the 'author' is runtime code using reflection or a compile-time source generator. This automated author should have the benefit of the same information that a human author would. How else can it produce code of similar value to 'handwritten' code? The |
Neither C# nullable annotations nor the C# required keyword are part of the runtime type system. Activator.CreateInstance doesn't know about required, for example, whereas the C# level new constraint does.
NRT was bolted on 20 years after C# and NET were introduced. The annotations provide a guide, and are most useful in published APIs coveying intent, but they are fallible and cannot be trusted 100% and users do need to take extra steps downstream. Augmenting my previous example: string[] values = new string[42];
List<string> list = new List<string>();
list.Add(values[0]); No compiler warnings, no exceptions, yet the list contains null, and a consumer of the list needs to be able to deal with that.
Which means some would be respected and some wouldn't, making the story even more convoluted.
But it's non-local. Project-level settings (like for checked/unchecked) that impact code semantics are generally considered now to have been a mistake, because the code you write has different meanings based on the project in which it's used. Nullable as a project level setting factored that in and was intended to impact diagnostics but not runtime semantics. The guidance for developers enabling nullable is to iterate until their code compiles and then they're done: that breaks down here, as enabling the project setting will change the meaning of existing use of System.Text.Json, such that just successfully compiling is no longer enough. I could be wrong, but I suspect enabling this by default would be a massive breaking change.
I had no problem with S.T.J respecting required because its use in a type definition is unambiguous. But for NRT, a type "string" in a type definition is ambiguous and could be either nullable or non-nullable depending on project settings, such that its meaning changes when the project setting is set. That meaning change was intended to be compile-time only... this makes it a run-time break. |
The proposal is to have this as an opt-in setting. Otherwise it would be a breaking change as you're pointing out. |
Which comment has the actual proposal? |
Far from final, but here's a sketch: #1256 (comment). Will also need switches on the contract model for the source generator to be able to access. |
Thanks. If it's opt-in, my primary concerns are alleviated.
Those APIs are just using reflection to get at the relevant information. If those APIs aren't exposed publicly downlevel, S.T.J could still achieve the same thing using reflection, even by building in a copy of those implementations as internal (and with whatever tweaks might be needed to work downlevel). |
Updated API proposal following up from #1256 (comment): namespace System.Text.Json;
public partial class JsonSerializerOptions
{
public bool ResolveNullableReferenceTypes { get; set; }
}
public partial class JsonSourceGenerationOptionsAttribute
{
public bool ResolveNullableReferenceTypes { get; set; }
}
namespace System.Text.Json.Serialization.Metadata;
public partial class JsonPropertyInfo
{
// Fail serialization if the getter returns null.
// Defaults to true if the global flag has been enabled
// and the property is annotated as non-nullable
// (e.g. via type annotation or `NotNull` or `MaybeNull` attributes)
public bool AllowNullWrites { get; set; }
// Fail deserialization if the setter is passed a null.
// Defaults to true if the global flag has been enabled
// and the property is annotated as non-nullable
// (e.g. via type annotation or `DisallowNull` or `AllowNull` attributes)
public bool AllowNullReads { get; set; }
} The two added properties on This proposal dovetails with #100075, so it might be conceivable that could unify both behaviours under a single feature switch. cc @stephentoub |
I created a proposal in #100144 which establishes scope for the feature. Closing in favor of that. |
When deserializing "null" for a non-nullable value type System.Text.Json throws an Exception. However, for reference types this is not the case even if the project has the "Nullable" feature enabled.
Expectation: When nullable is enabled for the project deserializing
null
into a non-nullable type should fail for both value and reference types.Question: This could be worked around by writing a custom Converter that honors nullable during deserialization. Are there plans to expose a setting to control the deserialization behavior for non-nullable reference types?
Repro: Deserializing DateTime as null fails (by design per this issue 40922). Deserializing List as null succeeds.
The text was updated successfully, but these errors were encountered: