Guidance for handling Null vs Undefined in .NET JSON #123207
Replies: 5 comments 1 reply
-
|
I'm a bot. Here are possible related and/or duplicate issues (I may be wrong): |
Beta Was this translation helpful? Give feedback.
-
|
Tagging subscribers to this area: @dotnet/area-system-text-json |
Beta Was this translation helpful? Give feedback.
-
|
I didn't notice I was posting under the Fern support account. I'm the author of this issue. |
Beta Was this translation helpful? Give feedback.
-
In my opinion, the best way to encode the presence or absence of a property within JSON is to use one of the JSON DOM types. Using a POCO is bound to lose some fidelity since there is no native concept of a property being absent. If I absolutely needed to I would probably go for a wrapper type like the one you described, although I think the using System.Text.Json;
using System.Text.Json.Serialization;
string json = JsonSerializer.Serialize(new MyPoco() { Null = null, Defined = "defined" });
Console.WriteLine(json);
var result = JsonSerializer.Deserialize<MyPoco>(json);
Console.WriteLine(result.Undefined.HasValue);
Console.WriteLine(result.Null.HasValue);
public class MyPoco
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Option<string?> Undefined { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Option<string?> Null { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Option<string?> Defined { get; set; }
}
[JsonConverter(typeof(OptionConverterFactory))]
public readonly struct Option<T>
{
public bool HasValue { get; }
public T Value { get; }
public Option(T value)
{
HasValue = true;
Value = value;
}
public static implicit operator Option<T>(T value) => new(value);
}
public sealed class OptionConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsGenericType &&
typeToConvert.GetGenericTypeDefinition() == typeof(Option<>);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type valueType = typeToConvert.GetGenericArguments()[0];
Type converterType = typeof(OptionConverter<>).MakeGenericType(valueType);
return (JsonConverter?)Activator.CreateInstance(converterType);
}
}
public sealed class OptionConverter<T> : JsonConverter<Option<T>>
{
public override bool HandleNull => true;
public override Option<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return new(JsonSerializer.Deserialize<T>(ref reader, options)!);
}
public override void Write(Utf8JsonWriter writer, Option<T> value, JsonSerializerOptions options)
{
if (!value.HasValue)
{
// If absolutely necessary, write undefined values as null although
// this should be gated using JsonIgnoreCondition.WhenWritingDefault
writer.WriteNullValue();
return;
}
JsonSerializer.Serialize(writer, value.Value, options);
}
} |
Beta Was this translation helpful? Give feedback.
-
|
We use a pattern similar to the OData deltas: https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs The specialized JSON serializer for the deltas store the the information in a key-value property bag. Then you can either obtain the values/nested values via the getters (by name, by expression, looping through them) or you apply the modifications directly to a given object. It's a bit tricky to get everything aligned (JSON serialization, OpenAPI docs with correct optional/required field declaratiosn etc.) but once you got the main things in place, the approach is really nice for strict RESTful services. You can then simply accept a |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
The Problem
We've been building HTTP SDKs and OpenAPI clients in C#, and we've been running into this issue with representing three distinct states for a field:
The problem is that C#'s nullable types (
string?,int?) can only represent two states: null or value. There's no way to distinguish between "not provided" and "explicitly set to null."Example
Let's say you have a PATCH endpoint for updating a user profile:
With standard C# types, you can't express this:
string? Email = null- Does this mean "clear it" or "not provided"?What I've Built So Far
I ended up implementing an
Optional<T>type that wraps nullable types to give us that third state:How It Works
Here's what using it looks like:
Serialization Behavior
Quick reference for how different combinations serialize:
string? Fieldnull[Nullable] string? Fieldnullnull[Nullable] string? FieldOf("text")"text"[Optional] string? Fieldnull[Optional] string? FieldOf("text")"text"[Optional] Optional<string> FieldUndefined[Optional] Optional<string> FieldOf("text")"text"[Optional, Nullable] Optional<string?> FieldUndefined[Optional, Nullable] Optional<string?> FieldOf(null)null[Optional, Nullable] Optional<string?> FieldOf("text")"text"What's in the Repo
The implementation includes:
Optional<T>struct with IEquatable supportJsonConverterandJsonTypeInfoResolverusing reflection to check[Optional]and[Nullable]attributesOptional<T>values in assertionsKey files in this repository:
Optional.cs- Core implementation with IOptional interfaceOpenApiSerialization.cs- JSON serialization with attribute-based modifiersOptionalAttribute.cs&NullableAttribute.cs- Attribute markersOpenApiSerializationTests.cs- Comprehensive test coverageOptionalComparerExtensions.cs- NUnit test helpersQuestions
I'm looking for guidance on whether this is the right approach:
1. Is there an existing .NET pattern I should be using instead?
2. JSON serialization - am I doing this right?
JsonTypeInfoResolverwith reflection on custom attributes reasonable?3. API design - the
Optional<T?>questionI chose to separate concerns:
Optional<T>= "Is the field present in the request?"T?= "Can the field value be null?"This leads to
Optional<T?>for optional nullable fields. Is this the right approach, or should I build null handling intoOptional<T>itself (like having anOptional<T>.Nullstate)?Full working implementation with tests is in this repo. All tests pass on .NET 8.0, .NET 10.0, and .NET Framework 4.6.2.
Would really appreciate any guidance on the recommended .NET approach here!
Beta Was this translation helpful? Give feedback.
All reactions