You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today, System.Text.Json provides two primary mechanisms for customizing serialization on the type level:
Customizing the contract via attribute annotations. This is generally our recommended approach when making straightforward tweaks to the contract, however it comes with a few drawbacks:
It requires that the user owns the type declarations they are looking to customize.
It forces repetition when applying the same rule across multiple members. This can be problematic for users defining large sets of DTOs or library authors looking to extend a rule to arbitrary sets of types.
There are inherent limits to the degree of customization achievable. For example, while it is possible to skip a property via JsonIgnoreAttribute, it is impossible to add a JSON property to the contract that doesn't correspond to a .NET property.
For certain users that prefer serializing their domain model directly, introducing System.Text.Json dependencies to the domain layer is considered poor practice.
Authoring custom converters. While this mechanism is general-purpose enough to satisfy most customization requirements, it suffers from a couple of problems:
Making straightforward amendments like modifying serializable properties, specifying constructors or injecting serialization callbacks can be cumbersome, since it effectively requires replicating the entire object/collection serialization logic.
Currently, custom converters do not support async/resumable serialization which can result in performance bottlenecks when serializing large objects (we're planning on addressing this independently via Developers should be able to pass state to custom converters. #63795).
API Proposal
namespaceSystem.Text.Json{publicsealedpartialclassJsonSerializerOptions{publicIJsonTypeInfoResolverTypeInfoResolver{[RequiresUnreferencedCode]get;set;}}}namespaceSystem.Text.Json.Serialization{publicabstractpartialclassJsonSerializerContext:IJsonTypeInfoResolver{// Explicit interface implementation calling into the equivalent JsonSerializerContext abstract methodJsonTypeInfoSystem.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver.GetTypeInfo(Typetype,JsonSerializerOptionsoptions);}}namespaceSystem.Text.Json.Serialization.Metadata{publicinterfaceIJsonTypeInfoResolver{JsonTypeInfo?GetTypeInfo(Typetype,JsonSerializerOptionsoptions);}// Provides the default reflection-based contract metadata resolutionpublicclassDefaultJsonTypeInfoResolver:IJsonTypeInfoResolver{[RequiresUnreferencedCode]publicDefaultJsonTypeInfoResolver(){}publicvirtualJsonTypeInfoGetTypeInfo(Typetype,JsonSerializerOptionsoptions);publicIList<Action<JsonTypeInfo>>Modifiers{get;}}publicstaticclassJsonTypeInfoResolver{publicstaticIJsonTypeInfoResolverCombine(paramsIJsonTypeInfoResolver[]resolvers);}// Determines the kind of contract metadata a given JsonTypeInfo instance is customizingpublicenumJsonTypeInfoKind{None=0,// Type is either a primitive value or uses a custom converter -- contract metadata does not apply here.Object=1,// Type is serialized as a POCO with propertiesEnumerable=2,// Type is serialized as a collection with elementsDictionary=3// Type is serialized as a dictionary with key/value pair entries}// remove: [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]// note: added abstractpublicabstractpartialclassJsonTypeInfo{publicTypeType{get;}publicJsonSerializerOptionsOptions{get;}// The converter instance associated with the type for the given options instance -- this cannot be changed.publicJsonConverterConverter{get;}// The kind of contract metadata we're customizingpublicJsonTypeInfoKindKind{get;}// Untyped default constructor delegate -- deserialization not supported if set to null.publicFunc<object>?CreateObject{get;set;}// List of property metadata for types serialized as POCOs.publicIList<JsonPropertyInfo>Properties{get;}// Equivalent to JsonNumberHandlingAttribute annotations.publicJsonNumberHandling?NumberHandling{get;set;}// Factory methods for JsonTypeInfopublicstaticJsonTypeInfo<T>CreateJsonTypeInfo<Τ>(JsonSerializerOptionsoptions){}publicstaticJsonTypeInfoCreateJsonTypeInfo(Typetype,JsonSerializerOptionsoptions){}// Factory methods for JsonPropertyInfopublicJsonPropertyInfoCreateJsonPropertyInfo(TypepropertyType,stringname){}}// remove: [EditorBrowsable(EditorBrowsableState.Never)]publicabstractpartialclassJsonTypeInfo<T>:JsonTypeInfo{// Default typed constructor delegatepublicnewFunc<T>?CreateObject{get;set;}}// remove: [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]publicabstractpartialclassJsonPropertyInfo{publicJsonSerializerOptionsOptions{get;}publicTypePropertyType{get;}publicstringName{get;set;}// Custom converter override at the property level, equivalent to `JsonConverterAttribute` annotation.publicJsonConverter?CustomConverter{get;set;}// Untyped getter delegatepublicFunc<object,object?>?Get{get;set;}// Untyped setter delegatepublicAction<object,object?>?Set{get;set;}// Predicate determining whether a property value should be serializedpublicFunc<object,object?,bool>?ShouldSerialize{get;set;}// Equivalent to JsonNumberHandlingAttribute overrides.publicJsonNumberHandling?NumberHandling{get;set;}}}
Adding support for DataMemberAttribute annotations
publicclassSystemRuntimeSerializationAttributeResolver:DefaultJsonTypeInfoResolver{publicoverrideJsonTypeInfoGetTypeInfo(Typetype,JsonSerializerOptionsoptions){JsonTypeInfojsonTypeInfo=base.GetJsonTypeInfo(type,options);if(jsonTypeInfo.Kind==JsonTypeInfoKind.Object&&type.GetCustomAttribute<DataContractAttribute>()is not null){jsonTypeInfo.Properties.Clear();foreach((PropertyInfopropertyInfo,DataMemberAttributeattr)intype.GetProperties(BindingFlags.Instance|BindingFlags.Public).Select((prop)=>(prop,prop.GetCustomAttribute<DataMemberAttribute>()asDataMemberAttribute)).Where((x)=>x.Item2!=null).OrderBy((x)=>x.Item2!.Order)){JsonPropertyInfojsonPropertyInfo=jsonTypeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType,attr.Name??propertyInfo.Name);jsonPropertyInfo.Get=propertyInfo.CanRead?propertyInfo.GetValue:null;jsonPropertyInfo.Set=propertyInfo.CanWrite?(obj,value)=>propertyInfo.SetValue(obj,value):null;jsonTypeInfo.Properties.Add(jsonPropertyInfo);}}returnjsonTypeInfo;}}
JsonPropertyInfo vs JsonPropertyInfo<T> vs JsonPropertyInfo<TDeclaringType, TPropertyType>
We have considered different approaches here and it all boils down to perf of the property setter.
According to simple perf tests run on different combinations of declaring types and property types as well 4 different approaches of setters using setter in form of: delegate void PropertySetter<DeclaringType, PropertyType>(ref DeclaringType obj, PropertyType val);
proves to be overall fastest. Current implementation would require a bit of work for this to be changed and such support can be added later. Given above we've decided to for a time being support only non-generic PropertyInfo with the slowest setter since such type already exists and corresponding setter would have to be added regardless of choice. In the future PropertyInfo<TDeclaringType, TPropertyType> should be added to support for the fastest possible case.
System.Text.Json already defines a JSON contract model internally, which is surfaced in public APIs via the JsonTypeInfo/JsonPropertyInfo types as opaque tokens for consumption by the source generator APIs. This is a proposal to define an IContractResolver-like construct that builds on top of the existing contract model and lets users generate custom contracts for System.Text.Json using runtime reflection.
Here is a provisional list of settings that should be user customizable in the contract model:
Object constructors.
The JSON type of the serialized value: object or array/collection (and possibly primitive values?).
A list of properties for object types.
Enumeration/population strategy for array/collection types.
Background and Motivation
Today, System.Text.Json provides two primary mechanisms for customizing serialization on the type level:
Customizing the contract via attribute annotations. This is generally our recommended approach when making straightforward tweaks to the contract, however it comes with a few drawbacks:
JsonIgnoreAttribute, it is impossible to add a JSON property to the contract that doesn't correspond to a .NET property.Authoring custom converters. While this mechanism is general-purpose enough to satisfy most customization requirements, it suffers from a couple of problems:
API Proposal
Usage examples
Custom resolver with constructed JsonTypeInfo
Adding support for
DataMemberAttributeannotationsCombining resolver
Doc comparing 3 design variants with recommendation can be found here: https://gist.github.com/krwq/c61f33faccc708bfa569b3c8aebb45d6
JsonPropertyInfo vs JsonPropertyInfo<T> vs JsonPropertyInfo<TDeclaringType, TPropertyType>
We have considered different approaches here and it all boils down to perf of the property setter.
According to simple perf tests run on different combinations of declaring types and property types as well 4 different approaches of setters using setter in form of:
delegate void PropertySetter<DeclaringType, PropertyType>(ref DeclaringType obj, PropertyType val);proves to be overall fastest. Current implementation would require a bit of work for this to be changed and such support can be added later. Given above we've decided to for a time being support only non-generic
PropertyInfowith the slowest setter since such type already exists and corresponding setter would have to be added regardless of choice. In the futurePropertyInfo<TDeclaringType, TPropertyType>should be added to support for the fastest possible case.Here are benchmark results: https://gist.github.com/krwq/d9d1bad3d59ff30f8db2a53a27adc755
Here is the benchmark code: https://gist.github.com/krwq/eb06529f0c99614579f84b69720ab46e
Acceptance Criteria
System.Text.Json already defines a JSON contract model internally, which is surfaced in public APIs via the JsonTypeInfo/JsonPropertyInfo types as opaque tokens for consumption by the source generator APIs. This is a proposal to define an
IContractResolver-like construct that builds on top of the existing contract model and lets users generate custom contracts for System.Text.Json using runtime reflection.Here is a provisional list of settings that should be user customizable in the contract model:
Use cases
Open Questions
JsonTypeInfomodel amenable to supporting collection serialization? Should we consider exposing aJsonCollectionInfo : JsonTypeInfoderived class? Related to Investigate ways to extend deserialization support for arbitrary collection types #38514.Progress
JsonProperty.ShouldSerializenull values #71964JsonSerializerOptions.TypeInfoResolverproperty should be nullable #71960https://github.com/dotnet/runtime/pull/72044/files#diff-f343f56e41ad406150ed9d35e7548f0d118dd3038c57dff2ff309e5e96331091R588
cc @steveharter @JamesNK