Skip to content
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

Fix CreateObject support for JsonConstructor types #10

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 @@ -23,6 +23,14 @@ internal abstract partial class ObjectWithParameterizedConstructorConverter<T> :

internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
krwq marked this conversation as resolved.
Show resolved Hide resolved

if (jsonTypeInfo.CreateObject != null)
{
// Contract customization: fall back to default object converter if user has set a default constructor delegate.
return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value);
}

object obj;
ArgumentState argumentState = state.Current.CtorArgumentState!;

Expand Down Expand Up @@ -91,7 +99,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
else
{
// Slower path that supports continuation and metadata reads.
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;

if (state.Current.ObjectState == StackFrameObjectState.None)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ internal static bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state)
// Whether a type (ConverterStrategy.Object) is deserialized using a parameterized constructor.
internal virtual bool ConstructorIsParameterized { get; }

/// <summary>
/// For reflection-based metadata generation, indicates whether the
/// converter avails of default constructors when deserializing types.
/// </summary>
internal bool UsesDefaultConstructor =>
ConverterStrategy switch
{
ConverterStrategy.Object => !ConstructorIsParameterized && this is not ObjectConverter,
krwq marked this conversation as resolved.
Show resolved Hide resolved
ConverterStrategy.Enumerable or
ConverterStrategy.Dictionary => true,
_ => false
};

internal ConstructorInfo? ConstructorInfo { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ public Func<object>? CreateObject
}
}

private protected abstract void SetCreateObject(Delegate? createObject, bool useForExtensionDataProperty = false);
private protected abstract void SetCreateObject(Delegate? createObject);
private protected Func<object>? _createObject;

// this is only assigned if Kind == None
internal Func<object>? CreateObjectForExtensionDataProperty { get; set; }
internal Func<object>? CreateObjectForExtensionDataProperty { get; private protected set; }

/// <summary>
/// Gets JsonPropertyInfo list. Only applicable when Kind is Object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ public abstract class JsonTypeInfo<T> : JsonTypeInfo
}
}

private protected override void SetCreateObject(Delegate? createObject, bool useForExtensionDataProperty = false)
private protected override void SetCreateObject(Delegate? createObject)
{
Debug.Assert(createObject is null or Func<object> or Func<T>);

CheckMutable();

if (Kind == JsonTypeInfoKind.None)
{
Debug.Assert(_createObject == null);
Debug.Assert(_typedCreateObject == null);
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKindNone();
}

Func<object>? untypedCreateObject;
Func<T>? typedCreateObject;

Expand All @@ -54,25 +61,8 @@ private protected override void SetCreateObject(Delegate? createObject, bool use
typedCreateObject = () => (T)untypedCreateObject();
}


if (Kind != JsonTypeInfoKind.None)
{
_createObject = untypedCreateObject;
_typedCreateObject = typedCreateObject;
}
else
{
if (useForExtensionDataProperty)
{
CreateObjectForExtensionDataProperty = untypedCreateObject;
}
else
{
Debug.Assert(_createObject == null);
Debug.Assert(_typedCreateObject == null);
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKindNone();
}
}
_createObject = untypedCreateObject;
_typedCreateObject = typedCreateObject;
}

internal JsonTypeInfo(JsonConverter converter, JsonSerializerOptions options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ internal ReflectionJsonTypeInfo(JsonConverter converter, JsonSerializerOptions o
AddPropertiesAndParametersUsingReflection();
}

SetCreateObject(Options.MemberAccessorStrategy.CreateConstructor(typeof(T)), useForExtensionDataProperty: true);
Func<object>? createObject = Options.MemberAccessorStrategy.CreateConstructor(typeof(T));
if (converter.UsesDefaultConstructor)
{
SetCreateObject(createObject);
}
krwq marked this conversation as resolved.
Show resolved Hide resolved

CreateObjectForExtensionDataProperty = createObject;
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public SourceGenJsonTypeInfo(JsonSerializerOptions options, JsonObjectInfoValues
}
else
{
SetCreateObject(objectInfo.ObjectCreator, useForExtensionDataProperty: true);
SetCreateObject(objectInfo.ObjectCreator);
CreateObjectForExtensionDataProperty = ((JsonTypeInfo)this).CreateObject;
}

PropInitFunc = objectInfo.PropertyMetadataInitializer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static partial class DefaultJsonTypeInfoResolverTests
[InlineData(typeof(int))]
[InlineData(typeof(string))]
[InlineData(typeof(SomeClass))]
[InlineData(typeof(StructWithFourArgs))]
//[InlineData(typeof(StructWithFourArgs))] // TODO redesign test to accommodate JsonConstructor metadata handling
[InlineData(typeof(Dictionary<string, int>))]
[InlineData(typeof(DictionaryWrapper))]
[InlineData(typeof(List<int>))]
Expand Down Expand Up @@ -560,21 +560,19 @@ public static IEnumerable<object[]> GetTypeInfoTestData()
}

[Fact]
public static void JsonConstructorAttributeIsNotRespectedWhenCreateObjectIsSet()
public static void JsonConstructorAttributeIsOverriddenWhenCreateObjectIsSet()
{
DefaultJsonTypeInfoResolver resolver = new();
resolver.Modifiers.Add(ti =>
{
if (ti.Type == typeof(ClassWithParametrizedConstructorAndReadOnlyProperties))
{
Assert.Null(ti.CreateObject);
ti.CreateObject = () => new ClassWithParametrizedConstructorAndReadOnlyProperties(1, "test");
ti.CreateObject = () => new ClassWithParametrizedConstructorAndReadOnlyProperties(1, "test", dummyParam: true);
}
});

JsonSerializerOptions o = new();
o.TypeInfoResolver = resolver;

JsonSerializerOptions o = new() { TypeInfoResolver = resolver };
string json = """{"A":2,"B":"foo"}""";
var deserialized = JsonSerializer.Deserialize<ClassWithParametrizedConstructorAndReadOnlyProperties>(json, o);

Expand All @@ -588,29 +586,33 @@ private class ClassWithParametrizedConstructorAndReadOnlyProperties
public int A { get; }
public string B { get; }

[JsonConstructor]
public ClassWithParametrizedConstructorAndReadOnlyProperties(int a, string b)
public ClassWithParametrizedConstructorAndReadOnlyProperties(int a, string b, bool dummyParam)
{
A = a;
B = b;
}

[JsonConstructor]
public ClassWithParametrizedConstructorAndReadOnlyProperties(int a, string b)
{
Assert.Fail("this ctor should not be used");
}
}

[Fact]
public static void JsonConstructorAttributeIsNotRespectedAndPropertiesAreSetWhenCreateObjectIsSet()
public static void JsonConstructorAttributeIsOverridenAndPropertiesAreSetWhenCreateObjectIsSet()
{
DefaultJsonTypeInfoResolver resolver = new();
resolver.Modifiers.Add(ti =>
{
if (ti.Type == typeof(ClassWithParametrizedConstructorAndWritableProperties))
{
Assert.Null(ti.CreateObject);
ti.CreateObject = () => new ClassWithParametrizedConstructorAndWritableProperties(1, "test");
ti.CreateObject = () => new ClassWithParametrizedConstructorAndWritableProperties();
}
});

JsonSerializerOptions o = new();
o.TypeInfoResolver = resolver;
JsonSerializerOptions o = new() { TypeInfoResolver = resolver };

string json = """{"A":2,"B":"foo","C":"bar"}""";
var deserialized = JsonSerializer.Deserialize<ClassWithParametrizedConstructorAndWritableProperties>(json, o);
Expand All @@ -627,11 +629,56 @@ private class ClassWithParametrizedConstructorAndWritableProperties
public string B { get; set; }
public string C { get; set; }

public ClassWithParametrizedConstructorAndWritableProperties() { }

[JsonConstructor]
public ClassWithParametrizedConstructorAndWritableProperties(int a, string b)
{
Assert.Fail("this ctor should not be used");
}
}

[Fact]
public static void JsonConstructorAttributeIsOverridenAndPropertiesAreSetWhenCreateObjectIsSet_LargeConstructor()
{
DefaultJsonTypeInfoResolver resolver = new();
resolver.Modifiers.Add(ti =>
{
if (ti.Type == typeof(ClassWithLargeParameterizedConstructor))
{
Assert.Null(ti.CreateObject);
ti.CreateObject = () => new ClassWithLargeParameterizedConstructor();
}
});

JsonSerializerOptions o = new() { TypeInfoResolver = resolver };

string json = """{"A":2,"B":"foo","C":"bar","E":true}""";
var deserialized = JsonSerializer.Deserialize<ClassWithLargeParameterizedConstructor>(json, o);

Assert.NotNull(deserialized);
Assert.Equal(2, deserialized.A);
Assert.Equal("foo", deserialized.B);
Assert.Equal("bar", deserialized.C);
Assert.True(deserialized.E);
}

private class ClassWithLargeParameterizedConstructor
{
public int A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public bool E { get; set; }
public int F { get; set; }

public ClassWithLargeParameterizedConstructor() { }

[JsonConstructor]
public ClassWithLargeParameterizedConstructor(int a, string b, string c, string d, bool e, int f)
{
Assert.Fail("this ctor should not be used");
}
}
}
}