Skip to content

Commit

Permalink
Add [JsonConstructor] and support for deserializing with parameterize…
Browse files Browse the repository at this point in the history
…d ctors (#33444)

* Add [JsonConstructor] and support for deserializing with parameterized ctors

* Add some more tests and clean up

* Move property and parameter caches from converter to JsonClassInfo

* Address review feedback

* Address review feedback and reduce regression for existing benchmarks

* Address review feedback and add more tests

* Clean up arg state on ReadStack
  • Loading branch information
layomia authored Mar 20, 2020
1 parent 3f8dfca commit 4a80495
Show file tree
Hide file tree
Showing 50 changed files with 6,684 additions and 515 deletions.
5 changes: 5 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,11 @@ protected internal JsonConverter() { }
public abstract T Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options);
public abstract void Write(System.Text.Json.Utf8JsonWriter writer, [System.Diagnostics.CodeAnalysis.DisallowNull] T value, System.Text.Json.JsonSerializerOptions options);
}
[System.AttributeUsageAttribute(System.AttributeTargets.Constructor, AllowMultiple = false)]
public sealed partial class JsonConstructorAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonConstructorAttribute() { }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute
{
Expand Down
19 changes: 17 additions & 2 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@
<data name="JsonSerializerDoesNotSupportComments" xml:space="preserve">
<value>Comments cannot be stored when deserializing objects, only the Skip and Disallow comment handling modes are supported.</value>
</data>
<data name="DeserializeMissingParameterlessConstructor" xml:space="preserve">
<value>Deserialization of reference types without parameterless constructor is not supported. Type '{0}'</value>
<data name="DeserializeMissingDeserializationConstructor" xml:space="preserve">
<value>Deserialization of reference types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with '{0}' is not supported. Type '{1}'</value>
</data>
<data name="DeserializePolymorphicInterface" xml:space="preserve">
<value>Deserialization of interface types is not supported. Type '{0}'</value>
Expand Down Expand Up @@ -482,10 +482,25 @@
<data name="MetadataInvalidPropertyWithLeadingDollarSign" xml:space="preserve">
<value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default.</value>
</data>
<data name="MultipleMembersBindWithConstructorParameter" xml:space="preserve">
<value>Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in constructor '{4}' on deserialization.</value>
</data>
<data name="ConstructorParamIncompleteBinding" xml:space="preserve">
<value>Each parameter in constructor '{0}' on type '{1}' must bind to an object member on deserialization. Each parameter name must be the camel case equivalent of an object member named with the pascal case naming convention.</value>
</data>
<data name="ConstructorMaxOf64Parameters" xml:space="preserve">
<value>The constructor '{0}' on type '{1}' may not have more than 64 parameters for deserialization.</value>
</data>
<data name="ObjectWithParameterizedCtorRefMetadataNotHonored" xml:space="preserve">
<value>Reference metadata is not honored when deserializing types using parameterized constructors. See type '{0}'.</value>
</data>
<data name="SerializerConverterFactoryReturnsNull" xml:space="preserve">
<value>The converter '{0}' cannot return a null value.</value>
</data>
<data name="SerializationNotSupportedParentType" xml:space="preserve">
<value>The unsupported member type is located on type '{0}'.</value>
</data>
<data name="ExtensionDataCannotBindToCtorParam" xml:space="preserve">
<value>The extension data property '{0}' on type '{1}' cannot bind with a parameter in constructor '{2}'.</value>
</data>
</root>
13 changes: 11 additions & 2 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DocumentationFile>$(OutputPath)$(MSBuildProjectName).xml</DocumentationFile>
Expand Down Expand Up @@ -53,7 +53,9 @@
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.cs" />
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.MultiSegment.cs" />
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.TryGet.cs" />
<Compile Include="System\Text\Json\Serialization\Arguments.cs" />
<Compile Include="System\Text\Json\Serialization\ClassType.cs" />
<Compile Include="System\Text\Json\Serialization\ArgumentState.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterList.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ArrayConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ConcurrentQueueOfTConverter.cs" />
Expand All @@ -80,6 +82,9 @@
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOfTConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectConverterFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectDefaultConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Small.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\BooleanConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\ByteArrayConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\ByteConverter.cs" />
Expand Down Expand Up @@ -111,9 +116,10 @@
<Compile Include="System\Text\Json\Serialization\DefaultReferenceResolver.cs" />
<Compile Include="System\Text\Json\Serialization\JsonAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonCamelCaseNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.AddProperty.cs" />
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.Cache.cs" />
<Compile Include="System\Text\Json\Serialization\JsonCollectionConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonConstructorAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonConverter.ReadAhead.cs" />
<Compile Include="System\Text\Json\Serialization\JsonConverterAttribute.cs" />
Expand All @@ -127,6 +133,8 @@
<Compile Include="System\Text\Json\Serialization\JsonIgnoreAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonObjectConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonParameterInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonParameterInfoOfT.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfoOfTTypeToConvert.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyNameAttribute.cs" />
Expand All @@ -151,6 +159,7 @@
<Compile Include="System\Text\Json\Serialization\MemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
<Compile Include="System\Text\Json\Serialization\PooledByteBufferWriter.cs" />
<Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
<Compile Include="System\Text\Json\Serialization\PropertyRef.cs" />
<Compile Include="System\Text\Json\Serialization\ReadStack.cs" />
<Compile Include="System\Text\Json\Serialization\ReadStackFrame.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,12 @@ internal static class JsonConstants
public const int LowSurrogateStartValue = 0xDC00;
public const int LowSurrogateEndValue = 0xDFFF;
public const int BitShiftBy10 = 0x400;

// The maximum number of parameters a constructor can have where it can be considered
// for a path on deserialization where we don't box the constructor arguments.
public const int UnboxedParameterCountThreshold = 4;

// The maximum number of parameters a constructor can have where it can be supported.
public const int MaxParameterCount = 64;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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 FoundProperties = System.ValueTuple<System.Text.Json.JsonPropertyInfo, System.Text.Json.JsonReaderState, long, byte[]?, string?>;
using FoundPropertiesAsync = System.ValueTuple<System.Text.Json.JsonPropertyInfo, object?, string?>;

namespace System.Text.Json
{
/// <summary>
/// Holds relevant state when deserializing objects with parameterized constructors.
/// Lives on the current ReadStackFrame.
/// </summary>
internal class ArgumentState
{
// Cache for parsed constructor arguments.
public object Arguments = null!;

// When deserializing objects with parameterized ctors, the properties we find on the first pass.
public FoundProperties[]? FoundProperties;

// When deserializing objects with parameterized ctors asynchronously, the properties we find on the first pass.
public FoundPropertiesAsync[]? FoundPropertiesAsync;
public int FoundPropertyCount;

// Current constructor parameter value.
public JsonParameterInfo? JsonParameterInfo;

// For performance, we order the parameters by the first deserialize and PropertyIndex helps find the right slot quicker.
public int ParameterIndex;
public List<ParameterRef>? ParameterRefCache;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace System.Text.Json
{
/// <summary>
/// Constructor arguments for objects with parameterized ctors with less than 5 parameters.
/// This is to avoid boxing for small, immutable objects.
/// </summary>
internal sealed class Arguments<TArg0, TArg1, TArg2, TArg3>
{
public TArg0 Arg0 = default!;
public TArg1 Arg1 = default!;
public TArg2 Arg2 = default!;
public TArg3 Arg3 = default!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new List<TElement>();
Expand All @@ -37,7 +37,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected override void CreateCollection(ref ReadStack state)
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new Dictionary<string, object>();
Expand All @@ -41,7 +41,7 @@ protected override void CreateCollection(ref ReadStack state)
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected override void CreateCollection(ref ReadStack state)
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new Dictionary<string, TValue>();
Expand All @@ -40,7 +40,7 @@ protected override void CreateCollection(ref ReadStack state)
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new List<object?>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new List<TElement>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti

if (constructorDelegate == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = constructorDelegate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new List<object?>();
Expand All @@ -36,7 +36,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new List<TElement>();
Expand All @@ -37,7 +37,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected override void CreateCollection(ref ReadStack state)
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new Dictionary<string, TValue>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (!TypeToConvert.IsAssignableFrom(RuntimeType))
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

state.Current.ReturnValue = new HashSet<TElement>();
Expand All @@ -34,7 +34,7 @@ protected override void CreateCollection(ref ReadStack state, JsonSerializerOpti
{
if (classInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoParameterlessConstructor(TypeToConvert);
ThrowHelper.ThrowNotSupportedException_DeserializeNoDeserializationConstructor(TypeToConvert);
}

TCollection returnValue = (TCollection)classInfo.CreateObject()!;
Expand Down
Loading

0 comments on commit 4a80495

Please sign in to comment.