Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Support custom converters that treat non-null input as null (#40287) #40357

Merged
merged 1 commit into from
Aug 15, 2019
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,23 @@ internal static JsonPropertyInfo CreateProperty(
Type propertyInfoClassType;
if (runtimePropertyType.IsGenericType && runtimePropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// For Nullable, use the underlying type.
Type underlyingPropertyType = Nullable.GetUnderlyingType(runtimePropertyType);
propertyInfoClassType = typeof(JsonPropertyInfoNullable<,>).MakeGenericType(parentClassType, underlyingPropertyType);
converter = options.DetermineConverterForProperty(parentClassType, underlyingPropertyType, propertyInfo);
// First try to find a converter for the Nullable, then if not found use the underlying type.
// This supports custom converters that want to (de)serialize as null when the value is not null.
converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
if (converter != null)
{
propertyInfoClassType = typeof(JsonPropertyInfoNotNullable<,,,>).MakeGenericType(
parentClassType,
declaredPropertyType,
runtimePropertyType,
runtimePropertyType);
}
else
{
Type typeToConvert = Nullable.GetUnderlyingType(runtimePropertyType);
converter = options.DetermineConverterForProperty(parentClassType, typeToConvert, propertyInfo);
propertyInfoClassType = typeof(JsonPropertyInfoNullable<,>).MakeGenericType(parentClassType, typeToConvert);
}
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ protected override void OnRead(JsonTokenType tokenType, ref ReadStack state, ref
}
else
{
// Null values were already handled.
Debug.Assert(value != null);

Set(state.Current.ReturnValue, value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ protected override void OnRead(JsonTokenType tokenType, ref ReadStack state, ref
}
else
{
// Null values were already handled.
Debug.Assert(value != null);

Set(state.Current.ReturnValue, (TDeclaredProperty)value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Globalization;
using Xunit;

namespace System.Text.Json.Serialization.Tests
Expand Down Expand Up @@ -59,5 +61,166 @@ public static void ValueTypeConverterForNullWithArray()
Assert.Equal(1, arr[1]);
Assert.Equal(0, arr[2]);
}

/// <summary>
/// Allow a conversion of empty string to a null DateTimeOffset?.
/// </summary>
public class JsonNullableDateTimeOffsetConverter : JsonConverter<DateTimeOffset?>
{
public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default;
}

string value = reader.GetString();
if (value == string.Empty)
{
return default;
}

return DateTimeOffset.ParseExact(value, "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture);
}

public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options)
{
if (!value.HasValue)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.Value.ToString("yyyy/MM/dd HH:mm:ss"));
}
}
}

private class ClassWithNullableAndJsonConverterAttribute
{
[JsonConverter(typeof(JsonNullableDateTimeOffsetConverter))]
public DateTimeOffset? NullableValue { get; set; }
}

[Fact]
public static void ValueConverterForNullableWithJsonConverterAttribute()
{
ClassWithNullableAndJsonConverterAttribute obj;

const string BaselineJson = @"{""NullableValue"":""1989/01/01 11:22:33""}";
obj = JsonSerializer.Deserialize<ClassWithNullableAndJsonConverterAttribute>(BaselineJson);
Assert.NotNull(obj.NullableValue);

const string Json = @"{""NullableValue"":""""}";
obj = JsonSerializer.Deserialize<ClassWithNullableAndJsonConverterAttribute>(Json);
Assert.Null(obj.NullableValue);

string json = JsonSerializer.Serialize(obj);
Assert.Contains(@"""NullableValue"":null", json);
}

private class ClassWithNullableAndWithoutJsonConverterAttribute
{
public DateTimeOffset? NullableValue { get; set; }
public List<DateTimeOffset?> NullableValues { get; set; }
}

[Fact]
public static void ValueConverterForNullableWithoutJsonConverterAttribute()
{
const string Json = @"{""NullableValue"":"""", ""NullableValues"":[""""]}";
ClassWithNullableAndWithoutJsonConverterAttribute obj;

// The json is not valid with the default converter.
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ClassWithNullableAndWithoutJsonConverterAttribute>(Json));

JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new JsonNullableDateTimeOffsetConverter());

obj = JsonSerializer.Deserialize<ClassWithNullableAndWithoutJsonConverterAttribute>(Json, options);
Assert.Null(obj.NullableValue);
Assert.Null(obj.NullableValues[0]);

string json = JsonSerializer.Serialize(obj);
Assert.Contains(@"""NullableValue"":null", json);
Assert.Contains(@"""NullableValues"":[null]", json);
}

[JsonConverter(typeof(ClassThatCanBeNullDependingOnContentConverter))]
private class ClassThatCanBeNullDependingOnContent
{
public int MyInt { get; set; }
}

/// <summary>
/// Allow a conversion of ClassThatCanBeNullDependingOnContent to null when its MyInt property is 0.
/// </summary>
private class ClassThatCanBeNullDependingOnContentConverter : JsonConverter<ClassThatCanBeNullDependingOnContent>
{
public override ClassThatCanBeNullDependingOnContent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

// Assume a single property.

reader.Read();
Assert.Equal(JsonTokenType.PropertyName, reader.TokenType);

reader.Read();
int myInt = reader.GetInt16();

reader.Read();
Assert.Equal(JsonTokenType.EndObject, reader.TokenType);

if (myInt == 0)
{
return null;
}

return new ClassThatCanBeNullDependingOnContent
{
MyInt = myInt
};
}

public override void Write(Utf8JsonWriter writer, ClassThatCanBeNullDependingOnContent value, JsonSerializerOptions options)
{
writer.WriteStartObject();

if (value.MyInt == 0)
{
writer.WriteNull("MyInt");
}
else
{
writer.WriteNumber("MyInt", value.MyInt);
}

writer.WriteEndObject();
}
}

[Fact]
public static void ConverterForClassThatCanBeNullDependingOnContent()
{
ClassThatCanBeNullDependingOnContent obj;

obj = JsonSerializer.Deserialize<ClassThatCanBeNullDependingOnContent>(@"{""MyInt"":5}");
Assert.Equal(5, obj.MyInt);

string json;
json = JsonSerializer.Serialize(obj);
Assert.Contains(@"""MyInt"":5", json);

obj.MyInt = 0;
json = JsonSerializer.Serialize(obj);
Assert.Contains(@"""MyInt"":null", json);

obj = JsonSerializer.Deserialize<ClassThatCanBeNullDependingOnContent>(@"{""MyInt"":0}");
Assert.Null(obj);
}
}
}