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

Commit

Permalink
Support custom converters that treat non-null input as null (#40287) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
steveharter authored and Eric Erhardt committed Aug 15, 2019
1 parent 75ec1b3 commit f8fddfe
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 10 deletions.
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);
}
}
}

0 comments on commit f8fddfe

Please sign in to comment.