* Add sourcegen support for required & init-only properties.
* Add test coverage for required fields & remove a few async void methods.
* Remove commented out code.
* Tweak JsonSerializerOptions resolution logic in wrapper implementation
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.0", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.0.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.4", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.4.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj", "{66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests.csproj", "{256A4653-4287-44B3-BDEF-67FC1522ED2F}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
EndProject
public const string IncompatibleConverterType =
"The converter '{0}' is not compatible with the type '{1}'.";
- public const string InitOnlyPropertyDeserializationNotSupported =
- "Deserialization of init-only properties is currently not supported in source generation mode.";
+ public const string InitOnlyPropertySetterNotSupported =
+ "Setting init-only properties is not supported in source generation mode.";
public const string InvalidJsonConverterFactoryOutput =
"The converter '{0}' cannot return null or a JsonConverterFactory instance.";
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using System.Reflection;
using System.Text.Json.Reflection;
using System.Text.Json.Serialization;
{
{ DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null",
{ CanUseSetter: true, IsInitOnlySetter: true }
- => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertyDeserializationNotSupported}"")",
+ => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertySetterNotSupported}"")",
{ CanUseSetter: true } when typeGenerationSpec.IsValueType
=> $@"static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{nameSpecifiedInSourceCode} = value!",
{ CanUseSetter: true }
{JsonPropertyInfoTypeRef} {propertyInfoVarName} = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>({OptionsLocalVariableName}, {infoVarName});");
- if (memberMetadata.IsRequired)
+ if (memberMetadata.HasJsonRequiredAttribute ||
+ (memberMetadata.IsRequired && !typeGenerationSpec.ConstructorSetsRequiredParameters))
{
sb.Append($@"
{propertyInfoVarName}.IsRequired = true;");
Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);
ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
- int paramCount = parameters.Length;
+ List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;
+ int paramCount = parameters.Length + (propertyInitializers?.Count(propInit => !propInit.MatchesConstructorParameter) ?? 0);
Debug.Assert(paramCount > 0);
StringBuilder sb = new($@"
{JsonParameterInfoValuesTypeRef}[] {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];
{JsonParameterInfoValuesTypeRef} info;
");
-
- for (int i = 0; i < paramCount; i++)
+ foreach (ParameterGenerationSpec spec in parameters)
{
- ParameterInfo reflectionInfo = parameters[i].ParameterInfo;
+ ParameterInfo reflectionInfo = spec.ParameterInfo;
Type parameterType = reflectionInfo.ParameterType;
string parameterTypeRef = parameterType.GetCompilableName();
HasDefaultValue = {FormatBool(reflectionInfo.HasDefaultValue)},
DefaultValue = {defaultValueAsStr}
}};
- {parametersVarName}[{i}] = {InfoVarName};
+ {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
+");
+ }
+
+ if (propertyInitializers != null)
+ {
+ Debug.Assert(propertyInitializers.Count > 0);
+
+ foreach (PropertyInitializerGenerationSpec spec in propertyInitializers)
+ {
+ if (spec.MatchesConstructorParameter)
+ continue;
+
+ sb.Append(@$"
+ {InfoVarName} = new()
+ {{
+ Name = ""{spec.Property.JsonPropertyName ?? spec.Property.ClrName}"",
+ ParameterType = typeof({spec.Property.TypeGenerationSpec.TypeRef}),
+ Position = {spec.ParameterIndex},
+ HasDefaultValue = false,
+ DefaultValue = default({spec.Property.TypeGenerationSpec.TypeRef}),
+ }};
+ {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
");
+ }
}
sb.Append(@$"
private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec)
{
Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);
-
ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
- int paramCount = parameters.Length;
- Debug.Assert(paramCount != 0);
+ List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;
const string ArgsVarName = "args";
- int lastIndex = paramCount - 1;
StringBuilder sb = new($"static ({ArgsVarName}) => new {typeGenerationSpec.TypeRef}(");
- for (int i = 0; i < lastIndex; i++)
+ if (parameters.Length > 0)
{
- sb.Append($"{GetParamUnboxing(parameters[i], i)}, ");
+ foreach (ParameterGenerationSpec param in parameters)
+ {
+ int index = param.ParameterIndex;
+ sb.Append($"{GetParamUnboxing(param.ParameterInfo.ParameterType, index)}, ");
+ }
+
+ sb.Length -= 2; // delete the last ", " token
}
- sb.Append($"{GetParamUnboxing(parameters[lastIndex], lastIndex)})");
+ sb.Append(')');
+
+ if (propertyInitializers != null)
+ {
+ Debug.Assert(propertyInitializers.Count > 0);
+ sb.Append("{ ");
+ foreach (PropertyInitializerGenerationSpec property in propertyInitializers)
+ {
+ sb.Append($"{property.Property.ClrName} = {GetParamUnboxing(property.Property.TypeGenerationSpec.Type, property.ParameterIndex)}, ");
+ }
+
+ sb.Length -= 2; // delete the last ", " token
+ sb.Append(" }");
+ }
return sb.ToString();
- static string GetParamUnboxing(ParameterGenerationSpec spec, int index)
- => $"({spec.ParameterInfo.ParameterType.GetCompilableName()}){ArgsVarName}[{index}]";
+ static string GetParamUnboxing(Type type, int index)
+ => $"({type.GetCompilableName()}){ArgsVarName}[{index}]";
}
private string? GetWriterMethod(Type type)
private const string JsonRequiredAttributeFullName = "System.Text.Json.Serialization.JsonRequiredAttribute";
private const string JsonSerializerContextFullName = "System.Text.Json.Serialization.JsonSerializerContext";
private const string JsonSourceGenerationOptionsAttributeFullName = "System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute";
+ private const string SetsRequiredMembersAttributeFullName = "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute";
internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
- private static DiagnosticDescriptor InitOnlyPropertyDeserializationNotSupported { get; } = new DiagnosticDescriptor(
- id: "SYSLIB1037",
- title: new LocalizableResourceString(nameof(SR.InitOnlyPropertyDeserializationNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
- messageFormat: new LocalizableResourceString(nameof(SR.InitOnlyPropertyDeserializationNotSupportedFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
- category: JsonConstants.SystemTextJsonSourceGenerationName,
- defaultSeverity: DiagnosticSeverity.Warning,
- isEnabledByDefault: true);
-
private static DiagnosticDescriptor InaccessibleJsonIncludePropertiesNotSupported { get; } = new DiagnosticDescriptor(
id: "SYSLIB1038",
title: new LocalizableResourceString(nameof(SR.InaccessibleJsonIncludePropertiesNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
string? runtimeTypeRef = null;
List<PropertyGenerationSpec>? propGenSpecList = null;
ObjectConstructionStrategy constructionStrategy = default;
+ bool constructorSetsRequiredMembers = false;
ParameterGenerationSpec[]? paramGenSpecArray = null;
+ List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList = null;
CollectionType collectionType = CollectionType.NotApplicable;
JsonNumberHandling? numberHandling = null;
bool foundDesignTimeCustomConverter = false;
bool implementsIJsonOnSerialized = false;
bool implementsIJsonOnSerializing = false;
bool isPolymorphic = false;
- bool hasInitOnlyProperties = false;
bool hasTypeFactoryConverter = false;
bool hasPropertyFactoryConverters = false;
bool canContainNullableReferenceAnnotations = type.CanContainNullableReferenceTypeAnnotations();
}
else if (type.GetCompatibleGenericInterface(_iasyncEnumerableOfTType) is Type iasyncEnumerableType)
{
- if (type.CanUseDefaultConstructorForDeserialization())
+ if (type.CanUseDefaultConstructorForDeserialization(out ConstructorInfo? defaultCtor))
{
constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
+ constructorSetsRequiredMembers = defaultCtor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
}
Type elementType = iasyncEnumerableType.GetGenericArguments()[0];
}
else if (_ienumerableType.IsAssignableFrom(type))
{
- if (type.CanUseDefaultConstructorForDeserialization())
+ if (type.CanUseDefaultConstructorForDeserialization(out ConstructorInfo? defaultCtor))
{
constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
+ constructorSetsRequiredMembers = defaultCtor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
}
Type? actualTypeToConvert;
if ((constructor != null || type.IsValueType) && !type.IsAbstract)
{
+ constructorSetsRequiredMembers = constructor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
ParameterInfo[]? parameters = constructor?.GetParameters();
int paramCount = parameters?.Length ?? 0;
paramGenSpecArray[i] = new ParameterGenerationSpec()
{
TypeGenerationSpec = typeGenerationSpec,
- ParameterInfo = parameterInfo
+ ParameterInfo = parameterInfo,
+ ParameterIndex = i
};
_implicitlyRegisteredTypes.Add(typeGenerationSpec);
BindingFlags.DeclaredOnly;
bool propertyOrderSpecified = false;
+ paramGenSpecArray ??= Array.Empty<ParameterGenerationSpec>();
+ int nextParameterIndex = paramGenSpecArray.Length;
// Walk the type hierarchy starting from the current type up to the base type(s)
foreach (Type currentType in type.GetSortedTypeHierarchy())
_implicitlyRegisteredTypes.Add(dataExtensionPropGenSpec);
}
- if (!hasInitOnlyProperties && spec.CanUseSetter && spec.IsInitOnlySetter && !PropertyIsConstructorParameter(spec, paramGenSpecArray))
+ if (constructionStrategy is not ObjectConstructionStrategy.NotApplicable && spec.CanUseSetter &&
+ ((spec.IsRequired && !constructorSetsRequiredMembers) || spec.IsInitOnlySetter))
{
- _sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InitOnlyPropertyDeserializationNotSupported, memberLocation, new string[] { type.Name }));
- hasInitOnlyProperties = true;
+ ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(spec, paramGenSpecArray);
+
+ if (spec.IsRequired || matchingConstructorParameter is null)
+ {
+ constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor;
+
+ var propInitializerSpec = new PropertyInitializerGenerationSpec
+ {
+ Property = spec,
+ MatchesConstructorParameter = matchingConstructorParameter is not null,
+ ParameterIndex = matchingConstructorParameter?.ParameterIndex ?? nextParameterIndex++,
+ };
+
+ (propertyInitializerSpecList ??= new()).Add(propInitializerSpec);
+ }
}
if (spec.HasJsonInclude && (!spec.CanUseGetter || !spec.CanUseSetter || !spec.IsPublic))
numberHandling,
propGenSpecList,
paramGenSpecArray,
+ propertyInitializerSpecList,
collectionType,
collectionKeyTypeSpec,
collectionValueTypeSpec,
constructionStrategy,
+ constructorSetsRequiredMembers,
nullableUnderlyingTypeMetadata: nullableUnderlyingTypeGenSpec,
runtimeTypeRef,
dataExtensionPropGenSpec,
return true;
}
- Type? actualDictionaryType = type.GetCompatibleGenericInterface(_idictionaryOfTKeyTValueType);
+ Type? actualDictionaryType = type.GetCompatibleGenericInterface(_idictionaryOfTKeyTValueType);
if (actualDictionaryType == null)
{
return false;
}
}
- private static bool PropertyIsConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray)
- => paramGenSpecArray != null && paramGenSpecArray.Any(paramSpec => propSpec.ClrName.Equals(paramSpec.ParameterInfo.Name, StringComparison.OrdinalIgnoreCase));
+ private static ParameterGenerationSpec? GetMatchingConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray)
+ {
+ return paramGenSpecArray?.FirstOrDefault(MatchesConstructorParameter);
+
+ bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
+ => propSpec.ClrName.Equals(paramSpec.ParameterInfo.Name, StringComparison.OrdinalIgnoreCase);
+ }
private static bool PropertyIsOverriddenAndIgnored(
string currentMemberName,
out int order,
out bool hasFactoryConverter,
out bool isExtensionData,
- out bool isRequired);
+ out bool hasJsonRequiredAttribute);
ProcessMember(
memberInfo,
hasJsonInclude,
out bool isReadOnly,
out bool isPublic,
+ out bool isRequired,
out bool canUseGetter,
out bool canUseSetter,
out bool getterIsVirtual,
IsProperty = memberInfo.MemberType == MemberTypes.Property,
IsPublic = isPublic,
IsVirtual = isVirtual,
- IsRequired = isRequired,
JsonPropertyName = jsonPropertyName,
RuntimePropertyName = runtimePropertyName,
PropertyNameVarName = propertyNameVarName,
IsReadOnly = isReadOnly,
+ IsRequired = isRequired,
+ HasJsonRequiredAttribute = hasJsonRequiredAttribute,
IsInitOnlySetter = setterIsInitOnly,
CanUseGetter = canUseGetter,
CanUseSetter = canUseSetter,
out int order,
out bool hasFactoryConverter,
out bool isExtensionData,
- out bool isRequired)
+ out bool hasJsonRequiredAttribute)
{
hasJsonInclude = false;
jsonPropertyName = null;
converterInstantiationLogic = null;
order = 0;
isExtensionData = false;
- isRequired = false;
+ hasJsonRequiredAttribute = false;
bool foundDesignTimeCustomConverter = false;
hasFactoryConverter = false;
break;
case JsonRequiredAttributeFullName:
{
- isRequired = true;
+ hasJsonRequiredAttribute = true;
}
break;
default:
bool hasJsonInclude,
out bool isReadOnly,
out bool isPublic,
+ out bool isRequired,
out bool canUseGetter,
out bool canUseSetter,
out bool getterIsVirtual,
out bool setterIsInitOnly)
{
isPublic = false;
+ isRequired = false;
canUseGetter = false;
canUseSetter = false;
getterIsVirtual = false;
{
MethodInfo? getMethod = propertyInfo.GetMethod;
MethodInfo? setMethod = propertyInfo.SetMethod;
+ isRequired = propertyInfo.IsRequired();
if (getMethod != null)
{
{
isPublic = fieldInfo.IsPublic;
isReadOnly = fieldInfo.IsInitOnly;
+ isRequired = fieldInfo.IsRequired();
if (!fieldInfo.IsPrivate && !fieldInfo.IsFamily)
{
public required TypeGenerationSpec TypeGenerationSpec { get; init; }
public required ParameterInfo ParameterInfo { get; init; }
+
+ public required int ParameterIndex { get; init; }
}
}
public bool IsVirtual { get; init; }
/// <summary>
- /// The property has JsonRequiredAttribute.
- /// </summary>
- public bool IsRequired { get; init; }
-
- /// <summary>
/// The property name specified via JsonPropertyNameAttribute, if available.
/// </summary>
public string? JsonPropertyName { get; init; }
public bool IsReadOnly { get; init; }
/// <summary>
+ /// Whether the property is marked `required`.
+ /// </summary>
+ public bool IsRequired { get; init; }
+
+ /// <summary>
+ /// The property is marked with JsonRequiredAttribute.
+ /// </summary>
+ public bool HasJsonRequiredAttribute { get; init; }
+
+ /// <summary>
/// Whether the property has an init-only set method.
/// </summary>
public bool IsInitOnlySetter { get; init; }
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.SourceGeneration
+{
+ internal sealed class PropertyInitializerGenerationSpec
+ {
+ public required PropertyGenerationSpec Property { get; init; }
+
+ public required int ParameterIndex { get; init; }
+
+ public required bool MatchesConstructorParameter { get; init; }
+ }
+}
private FieldAttributes? _attributes;
+ public IFieldSymbol Symbol => _field;
+
public override FieldAttributes Attributes
{
get
public bool NeedsAtSign { get; }
+ public IPropertySymbol Symbol => _property;
+
public override Type ReflectedType => throw new NotImplementedException();
public override MethodInfo[] GetAccessors(bool nonPublic)
return index < customAttributeData.ConstructorArguments.Count ? (TValue)customAttributeData.ConstructorArguments[index].Value! : default!;
}
+ public static bool ContainsAttribute(this MemberInfo memberInfo, string attributeFullName)
+ => CustomAttributeData.GetCustomAttributes(memberInfo).Any(attr => attr.AttributeType.FullName == attributeFullName);
+
public static bool IsInitOnly(this MethodInfo method)
{
if (method is null)
return methodInfoWrapper.IsInitOnly;
}
+ public static bool IsRequired(this PropertyInfo propertyInfo)
+ {
+#if ROSLYN4_4_OR_GREATER
+ if (propertyInfo is null)
+ {
+ throw new ArgumentNullException(nameof(propertyInfo));
+ }
+
+ PropertyInfoWrapper methodInfoWrapper = (PropertyInfoWrapper)propertyInfo;
+ return methodInfoWrapper.Symbol.IsRequired;
+#else
+ return false;
+#endif
+ }
+
+ public static bool IsRequired(this FieldInfo propertyInfo)
+ {
+#if ROSLYN4_4_OR_GREATER
+ if (propertyInfo is null)
+ {
+ throw new ArgumentNullException(nameof(propertyInfo));
+ }
+
+ FieldInfoWrapper fieldInfoWrapper = (FieldInfoWrapper)propertyInfo;
+ return fieldInfoWrapper.Symbol.IsRequired;
+#else
+ return false;
+#endif
+ }
+
private static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo)
{
IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(constructorInfo);
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
namespace System.Text.Json.Reflection
{
return false;
}
- public static bool CanUseDefaultConstructorForDeserialization(this Type type)
- => (type.GetConstructor(Type.EmptyTypes) != null || type.IsValueType) && !type.IsAbstract && !type.IsInterface;
+ public static bool CanUseDefaultConstructorForDeserialization(this Type type, out ConstructorInfo? constructorInfo)
+ {
+ constructorInfo = type.GetConstructor(Type.EmptyTypes);
+
+ if ((constructorInfo != null || type.IsValueType) && !type.IsAbstract && !type.IsInterface)
+ {
+ return true;
+ }
+
+ constructorInfo = null;
+ return false;
+ }
public static bool IsObjectType(this Type type) => type.FullName == "System.Object";
<Compile Include="ObjectConstructionStrategy.cs" />
<Compile Include="ParameterGenerationSpec.cs" />
<Compile Include="PropertyGenerationSpec.cs" />
+ <Compile Include="PropertyInitializerGenerationSpec.cs" />
<Compile Include="Reflection\AssemblyWrapper.cs" />
<Compile Include="Reflection\TypeExtensions.cs" />
<Compile Include="Reflection\FieldInfoWrapper.cs" />
public ParameterGenerationSpec[]? CtorParamGenSpecArray { get; private set; }
+ public List<PropertyInitializerGenerationSpec>? PropertyInitializerSpecList { get; private set; }
+ public int PropertyInitializersWithoutMatchingConstructorParameters { get; private set; }
+
public CollectionType CollectionType { get; private set; }
public TypeGenerationSpec? CollectionKeyTypeMetadata { get; private set; }
public ObjectConstructionStrategy ConstructionStrategy { get; private set; }
+ public bool ConstructorSetsRequiredParameters { get; private set; }
+
public TypeGenerationSpec? NullableUnderlyingTypeMetadata { get; private set; }
/// <summary>
JsonNumberHandling? numberHandling,
List<PropertyGenerationSpec>? propertyGenSpecList,
ParameterGenerationSpec[]? ctorParamGenSpecArray,
+ List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList,
CollectionType collectionType,
TypeGenerationSpec? collectionKeyTypeMetadata,
TypeGenerationSpec? collectionValueTypeMetadata,
ObjectConstructionStrategy constructionStrategy,
+ bool constructorSetsRequiredMembers,
TypeGenerationSpec? nullableUnderlyingTypeMetadata,
string? runtimeTypeRef,
TypeGenerationSpec? extensionDataPropertyTypeSpec,
IsPolymorphic = isPolymorphic;
NumberHandling = numberHandling;
PropertyGenSpecList = propertyGenSpecList;
+ PropertyInitializerSpecList = propertyInitializerSpecList;
CtorParamGenSpecArray = ctorParamGenSpecArray;
CollectionType = collectionType;
CollectionKeyTypeMetadata = collectionKeyTypeMetadata;
CollectionValueTypeMetadata = collectionValueTypeMetadata;
ConstructionStrategy = constructionStrategy;
+ ConstructorSetsRequiredParameters = constructorSetsRequiredMembers;
NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata;
RuntimeTypeRef = runtimeTypeRef;
ExtensionDataPropertyTypeSpec = extensionDataPropertyTypeSpec;
!state.CurrentContainsMetadata) // Do not use the fast path if state needs to write metadata.
{
Debug.Assert(jsonTypeInfo is JsonTypeInfo<T> typeInfo && typeInfo.SerializeHandler != null);
- Debug.Assert(options.SerializerContext?.CanUseSerializationLogic == true);
+ Debug.Assert(jsonTypeInfo.CanUseSerializeHandler);
((JsonTypeInfo<T>)jsonTypeInfo).SerializeHandler!(writer, value);
return true;
}
/// </summary>
public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
{
- private bool? _canUseSerializationLogic;
-
private JsonSerializerOptions? _options;
/// <summary>
/// Indicates whether pre-generated serialization logic for types in the context
/// is compatible with the run time specified <see cref="JsonSerializerOptions"/>.
/// </summary>
- internal bool CanUseSerializationLogic
+ internal bool CanUseFastPathSerializationLogic(JsonSerializerOptions options)
{
- get
- {
- if (!_canUseSerializationLogic.HasValue)
- {
- if (GeneratedSerializerOptions == null)
- {
- _canUseSerializationLogic = false;
- }
- else
- {
- _canUseSerializationLogic =
- // Guard against unsupported features
- Options.Converters.Count == 0 &&
- Options.Encoder == null &&
- // Disallow custom number handling we'd need to honor when writing.
- // AllowReadingFromString and Strict are fine since there's no action to take when writing.
- (Options.NumberHandling & (JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals)) == 0 &&
- Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None &&
+ Debug.Assert(options.TypeInfoResolver == this);
+
+ return
+ GeneratedSerializerOptions is not null &&
+ // Guard against unsupported features
+ options.Converters.Count == 0 &&
+ options.Encoder == null &&
+ // Disallow custom number handling we'd need to honor when writing.
+ // AllowReadingFromString and Strict are fine since there's no action to take when writing.
+ (options.NumberHandling & (JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals)) == 0 &&
+ options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None &&
#pragma warning disable SYSLIB0020
- !Options.IgnoreNullValues && // This property is obsolete.
+ !options.IgnoreNullValues && // This property is obsolete.
#pragma warning restore SYSLIB0020
- // Ensure options values are consistent with expected defaults.
- Options.DefaultIgnoreCondition == GeneratedSerializerOptions.DefaultIgnoreCondition &&
- Options.IgnoreReadOnlyFields == GeneratedSerializerOptions.IgnoreReadOnlyFields &&
- Options.IgnoreReadOnlyProperties == GeneratedSerializerOptions.IgnoreReadOnlyProperties &&
- Options.IncludeFields == GeneratedSerializerOptions.IncludeFields &&
- Options.PropertyNamingPolicy == GeneratedSerializerOptions.PropertyNamingPolicy &&
- Options.DictionaryKeyPolicy == GeneratedSerializerOptions.DictionaryKeyPolicy &&
- Options.WriteIndented == GeneratedSerializerOptions.WriteIndented;
- }
- }
-
- return _canUseSerializationLogic.Value;
- }
+ // Ensure options values are consistent with expected defaults.
+ options.DefaultIgnoreCondition == GeneratedSerializerOptions.DefaultIgnoreCondition &&
+ options.IgnoreReadOnlyFields == GeneratedSerializerOptions.IgnoreReadOnlyFields &&
+ options.IgnoreReadOnlyProperties == GeneratedSerializerOptions.IgnoreReadOnlyProperties &&
+ options.IncludeFields == GeneratedSerializerOptions.IncludeFields &&
+ options.PropertyNamingPolicy == GeneratedSerializerOptions.PropertyNamingPolicy &&
+ options.DictionaryKeyPolicy == GeneratedSerializerOptions.DictionaryKeyPolicy &&
+ options.WriteIndented == GeneratedSerializerOptions.WriteIndented;
}
/// <summary>
internal JsonSerializerContext? SerializerContext => _typeInfoResolver as JsonSerializerContext;
+ internal bool CanUseFastPathSerializationLogic
+ {
+ get
+ {
+ Debug.Assert(IsReadOnly);
+ return _canUseFastPathSerializationLogic ??= SerializerContext?.CanUseFastPathSerializationLogic(this) ?? false;
+ }
+ }
+
+ private bool? _canUseFastPathSerializationLogic;
+
// The cached value used to determine if ReferenceHandler should use Preserve or IgnoreCycles semanitcs or None of them.
internal ReferenceHandlingStrategy ReferenceHandlingStrategy = ReferenceHandlingStrategy.None;
// Workaround https://github.com/dotnet/linker/issues/2715
PropertyInfoForTypeInfo.EnsureChildOf(this);
PropertyInfoForTypeInfo.EnsureConfigured();
- CanUseSerializeHandler &= Options.SerializerContext?.CanUseSerializationLogic == true;
+ CanUseSerializeHandler &= Options.CanUseFastPathSerializationLogic;
JsonConverter converter = Converter;
Debug.Assert(PropertyInfoForTypeInfo.EffectiveConverter.ConverterStrategy == Converter.ConverterStrategy,
// this avoids creating a WriteStack and calling into the converter infrastructure.
Debug.Assert(SerializeHandler != null);
- Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+ Debug.Assert(CanUseSerializeHandler);
Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
SerializeHandler(writer, rootValue!);
// Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it.
Debug.Assert(SerializeHandler != null);
- Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+ Debug.Assert(CanUseSerializeHandler);
Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize);
// Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it.
Debug.Assert(SerializeHandler != null);
- Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+ Debug.Assert(CanUseSerializeHandler);
Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(Options, out PooledByteBufferWriter bufferWriter);
/// </summary>
public abstract partial class JsonSerializerWrapper
{
+ public abstract JsonSerializerOptions DefaultOptions { get; }
+
/// <summary>
/// Do the deserialize methods allow a value of 'null'.
/// For example, deserializing JSON to a String supports null by returning a 'null' String reference from a literal value of "null".
public abstract Task<object> DeserializeWrapper(string value, JsonTypeInfo jsonTypeInfo);
public abstract Task<object> DeserializeWrapper(string json, Type type, JsonSerializerContext context);
+
+ public JsonSerializerOptions GetDefaultOptionsWithMetadataModifier(Action<JsonTypeInfo> modifier)
+ {
+ JsonSerializerOptions defaultOptions = DefaultOptions;
+ return new JsonSerializerOptions(defaultOptions)
+ {
+ TypeInfoResolver = defaultOptions.TypeInfoResolver.WithModifier(modifier)
+ };
+ }
+ }
+
+ public static class JsonTypeInfoResolverExtensions
+ {
+ public static IJsonTypeInfoResolver WithModifier(this IJsonTypeInfoResolver resolver, Action<JsonTypeInfo> modifier)
+ => new JsonTypeInfoResolverWithModifier(resolver, modifier);
+
+ private class JsonTypeInfoResolverWithModifier : IJsonTypeInfoResolver
+ {
+ private readonly IJsonTypeInfoResolver _source;
+ private readonly Action<JsonTypeInfo> _modifier;
+
+ public JsonTypeInfoResolverWithModifier(IJsonTypeInfoResolver source, Action<JsonTypeInfo> modifier)
+ {
+ _source = source;
+ _modifier = modifier;
+ }
+
+ public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+ {
+ JsonTypeInfo? typeInfo = _source.GetTypeInfo(type, options);
+
+ if (typeInfo != null)
+ {
+ _modifier(typeInfo);
+ }
+
+ return typeInfo;
+ }
+ }
}
}
await Task.WhenAll(tasks);
}
- private async void TestIdTask()
+ private async Task TestIdTask()
{
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(@"{""$id"":1}", s_deserializerOptionsPreserve));
Assert.Equal("$.$id", ex.Path);
}
- private async void TestRefTask()
+ private async Task TestRefTask()
{
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(@"{""$ref"":1}", s_deserializerOptionsPreserve));
Assert.Equal("$.$ref", ex.Path);
}
[Fact] // https://github.com/dotnet/runtime/issues/51837
- public async void IgnoreCycles_StringShouldNotBeIgnored()
+ public async Task IgnoreCycles_StringShouldNotBeIgnored()
{
var stringReference = "John";
}
[Fact]
- public async void IgnoreCycles_BoxedValueShouldNotBeIgnored()
+ public async Task IgnoreCycles_BoxedValueShouldNotBeIgnored()
{
object dayOfBirthAsObject = 15;
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public abstract partial class RequiredKeywordTests : SerializerTests
+ {
+ public RequiredKeywordTests(JsonSerializerWrapper serializer) : base(serializer)
+ {
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ClassWithRequiredKeywordDeserialization(bool ignoreNullValues)
+ {
+ JsonSerializerOptions options = new()
+ {
+ IgnoreNullValues = ignoreNullValues
+ };
+
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembers>(options),
+ nameof(PersonWithRequiredMembers.FirstName),
+ nameof(PersonWithRequiredMembers.LastName));
+
+ var obj = new PersonWithRequiredMembers()
+ {
+ FirstName = "foo",
+ LastName = "bar"
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+ PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+ Assert.Equal(obj.FirstName, deserialized.FirstName);
+ Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+ Assert.Equal(obj.LastName, deserialized.LastName);
+
+ json = """{"LastName":"bar"}""";
+ JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.DoesNotContain("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+
+ json = """{"LastName":null}""";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.DoesNotContain("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+
+ json = "{}";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.Contains("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+ }
+
+ [Fact]
+ public async Task RequiredPropertyOccuringTwiceInThePayloadWorksAsExpected()
+ {
+ string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar","FirstName":"newfoo"}""";
+ PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json);
+ Assert.Equal("newfoo", deserialized.FirstName);
+ Assert.Equal("", deserialized.MiddleName);
+ Assert.Equal("bar", deserialized.LastName);
+ }
+
+ public class PersonWithRequiredMembers
+ {
+ public required string FirstName { get; set; }
+ public string MiddleName { get; set; } = "";
+ public required string LastName { get; set; }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeserialization(bool ignoreNullValues)
+ {
+ JsonSerializerOptions options = new()
+ {
+ IgnoreNullValues = ignoreNullValues
+ };
+
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtor>(options),
+ nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.FirstName),
+ nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.LastName),
+ nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info1),
+ nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info2));
+
+ var obj = new PersonWithRequiredMembersAndSmallParametrizedCtor("badfoo", "badbar")
+ {
+ // note: these must be set during initialize or otherwise we get compiler errors
+ FirstName = "foo",
+ LastName = "bar",
+ Info1 = "info1",
+ Info2 = "info2",
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar","Info1":"info1","Info2":"info2"}""", json);
+
+ var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
+ Assert.Equal(obj.FirstName, deserialized.FirstName);
+ Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+ Assert.Equal(obj.LastName, deserialized.LastName);
+ Assert.Equal(obj.Info1, deserialized.Info1);
+ Assert.Equal(obj.Info2, deserialized.Info2);
+
+ json = """{"FirstName":"foo","MiddleName":"","LastName":null,"Info1":null,"Info2":"info2"}""";
+ deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
+ Assert.Equal(obj.FirstName, deserialized.FirstName);
+ Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+ Assert.Null(deserialized.LastName);
+ Assert.Null(deserialized.Info1);
+ Assert.Equal(obj.Info2, deserialized.Info2);
+
+ json = """{"LastName":"bar","Info1":"info1"}""";
+ JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.DoesNotContain("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+ Assert.DoesNotContain("Info1", exception.Message);
+ Assert.Contains("Info2", exception.Message);
+
+ json = """{"LastName":null,"Info1":null}""";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.DoesNotContain("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+ Assert.DoesNotContain("Info1", exception.Message);
+ Assert.Contains("Info2", exception.Message);
+
+ json = "{}";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+ Assert.Contains("FirstName", exception.Message);
+ Assert.Contains("LastName", exception.Message);
+ Assert.DoesNotContain("MiddleName", exception.Message);
+ Assert.Contains("Info1", exception.Message);
+ Assert.Contains("Info2", exception.Message);
+ }
+
+ public class PersonWithRequiredMembersAndSmallParametrizedCtor
+ {
+ public required string FirstName { get; set; }
+ public string MiddleName { get; set; } = "";
+ public required string LastName { get; set; }
+ public required string Info1 { get; set; }
+ public required string Info2 { get; set; }
+
+ public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeserialization(bool ignoreNullValues)
+ {
+ JsonSerializerOptions options = new()
+ {
+ IgnoreNullValues = ignoreNullValues
+ };
+
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtor>(options),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.AProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.BProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.CProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.DProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.EProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.FProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.GProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.HProp),
+ nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.IProp));
+
+ var obj = new PersonWithRequiredMembersAndLargeParametrizedCtor("bada", "badb", "badc", "badd", "bade", "badf", "badg")
+ {
+ // note: these must be set during initialize or otherwise we get compiler errors
+ AProp = "a",
+ BProp = "b",
+ CProp = "c",
+ DProp = "d",
+ EProp = "e",
+ FProp = "f",
+ GProp = "g",
+ HProp = "h",
+ IProp = "i",
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","GProp":"g","HProp":"h","IProp":"i"}""", json);
+
+ var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
+ Assert.Equal(obj.AProp, deserialized.AProp);
+ Assert.Equal(obj.BProp, deserialized.BProp);
+ Assert.Equal(obj.CProp, deserialized.CProp);
+ Assert.Equal(obj.DProp, deserialized.DProp);
+ Assert.Equal(obj.EProp, deserialized.EProp);
+ Assert.Equal(obj.FProp, deserialized.FProp);
+ Assert.Equal(obj.GProp, deserialized.GProp);
+ Assert.Equal(obj.HProp, deserialized.HProp);
+ Assert.Equal(obj.IProp, deserialized.IProp);
+
+ json = """{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":null,"FProp":"f","GProp":"g","HProp":null,"IProp":"i"}""";
+ deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
+ Assert.Equal(obj.AProp, deserialized.AProp);
+ Assert.Equal(obj.BProp, deserialized.BProp);
+ Assert.Equal(obj.CProp, deserialized.CProp);
+ Assert.Equal(obj.DProp, deserialized.DProp);
+ Assert.Null(deserialized.EProp);
+ Assert.Equal(obj.FProp, deserialized.FProp);
+ Assert.Equal(obj.GProp, deserialized.GProp);
+ Assert.Null(deserialized.HProp);
+ Assert.Equal(obj.IProp, deserialized.IProp);
+
+ json = """{"AProp":"a","IProp":"i"}""";
+ JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+ Assert.DoesNotContain("AProp", exception.Message);
+ Assert.Contains("BProp", exception.Message);
+ Assert.Contains("CProp", exception.Message);
+ Assert.Contains("DProp", exception.Message);
+ Assert.Contains("EProp", exception.Message);
+ Assert.Contains("FProp", exception.Message);
+ Assert.Contains("GProp", exception.Message);
+ Assert.Contains("HProp", exception.Message);
+ Assert.DoesNotContain("IProp", exception.Message);
+
+ json = """{"AProp":null,"IProp":null}""";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+ Assert.DoesNotContain("AProp", exception.Message);
+ Assert.Contains("BProp", exception.Message);
+ Assert.Contains("CProp", exception.Message);
+ Assert.Contains("DProp", exception.Message);
+ Assert.Contains("EProp", exception.Message);
+ Assert.Contains("FProp", exception.Message);
+ Assert.Contains("GProp", exception.Message);
+ Assert.Contains("HProp", exception.Message);
+ Assert.DoesNotContain("IProp", exception.Message);
+
+ json = """{"BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","HProp":"h"}""";
+ exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+ Assert.Contains("AProp", exception.Message);
+ Assert.DoesNotContain("BProp", exception.Message);
+ Assert.DoesNotContain("CProp", exception.Message);
+ Assert.DoesNotContain("DProp", exception.Message);
+ Assert.DoesNotContain("EProp", exception.Message);
+ Assert.DoesNotContain("FProp", exception.Message);
+ Assert.Contains("GProp", exception.Message);
+ Assert.DoesNotContain("HProp", exception.Message);
+ Assert.Contains("IProp", exception.Message);
+ }
+
+ public class PersonWithRequiredMembersAndLargeParametrizedCtor
+ {
+ // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives
+ public required string AProp { get; set; }
+ public required string BProp { get; set; }
+ public required string CProp { get; set; }
+ public required string DProp { get; set; }
+ public required string EProp { get; set; }
+ public required string FProp { get; set; }
+ public required string GProp { get; set; }
+ public required string HProp { get; set; }
+ public required string IProp { get; set; }
+
+ public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop)
+ {
+ AProp = aprop;
+ BProp = bprop;
+ CProp = cprop;
+ DProp = dprop;
+ EProp = eprop;
+ FProp = fprop;
+ GProp = gprop;
+ }
+ }
+
+ [Fact]
+ public async Task ClassWithRequiredKeywordAndSetsRequiredMembersOnCtorWorks()
+ {
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSetsRequiredMembers>(Serializer.DefaultOptions)
+ /* no required members */);
+
+ var obj = new PersonWithRequiredMembersAndSetsRequiredMembers()
+ {
+ FirstName = "foo",
+ LastName = "bar"
+ };
+
+ string json = await Serializer.SerializeWrapper(obj);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+ json = """{"LastName":"bar"}""";
+ var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSetsRequiredMembers>(json);
+ Assert.Equal("", deserialized.FirstName);
+ Assert.Equal("", deserialized.MiddleName);
+ Assert.Equal("bar", deserialized.LastName);
+ }
+
+ public class PersonWithRequiredMembersAndSetsRequiredMembers
+ {
+ public required string FirstName { get; set; }
+ public string MiddleName { get; set; } = "";
+ public required string LastName { get; set; }
+
+ [SetsRequiredMembers]
+ public PersonWithRequiredMembersAndSetsRequiredMembers()
+ {
+ FirstName = "";
+ LastName = "";
+ }
+ }
+
+ [Fact]
+ public async Task ClassWithRequiredKeywordSmallParametrizedCtorAndSetsRequiredMembersOnCtorWorks()
+ {
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(Serializer.DefaultOptions)
+ /* no required members */);
+
+ var obj = new PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers("foo", "bar");
+
+ string json = await Serializer.SerializeWrapper(obj);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+ var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(json);
+ Assert.Equal("foo", deserialized.FirstName);
+ Assert.Equal("", deserialized.MiddleName);
+ Assert.Equal("bar", deserialized.LastName);
+ }
+
+ public class PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers
+ {
+ public required string FirstName { get; set; }
+ public string MiddleName { get; set; } = "";
+ public required string LastName { get; set; }
+
+ [SetsRequiredMembers]
+ public PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+ }
+
+ [Fact]
+ public async Task ClassWithRequiredKeywordLargeParametrizedCtorAndSetsRequiredMembersOnCtorWorks()
+ {
+ AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(Serializer.DefaultOptions)
+ /* no required members */);
+
+ var obj = new PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers("a", "b", "c", "d", "e", "f", "g");
+
+ string json = await Serializer.SerializeWrapper(obj);
+ Assert.Equal("""{"A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g"}""", json);
+
+ var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(json);
+ Assert.Equal("a", deserialized.A);
+ Assert.Equal("b", deserialized.B);
+ Assert.Equal("c", deserialized.C);
+ Assert.Equal("d", deserialized.D);
+ Assert.Equal("e", deserialized.E);
+ Assert.Equal("f", deserialized.F);
+ Assert.Equal("g", deserialized.G);
+ }
+
+ public class PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers
+ {
+ public required string A { get; set; }
+ public required string B { get; set; }
+ public required string C { get; set; }
+ public required string D { get; set; }
+ public required string E { get; set; }
+ public required string F { get; set; }
+ public required string G { get; set; }
+
+ [SetsRequiredMembers]
+ public PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers(string a, string b, string c, string d, string e, string f, string g)
+ {
+ A = a;
+ B = b;
+ C = c;
+ D = d;
+ E = e;
+ F = f;
+ G = g;
+ }
+ }
+
+ [Fact]
+ public async Task ClassWithRequiredFieldWorksAsExpected()
+ {
+ var options = new JsonSerializerOptions(Serializer.DefaultOptions) { IncludeFields = true };
+ options.MakeReadOnly();
+
+ JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(ClassWithRequiredField));
+ Assert.Equal(1, typeInfo.Properties.Count);
+
+ JsonPropertyInfo jsonPropertyInfo = typeInfo.Properties[0];
+ Assert.True(jsonPropertyInfo.IsRequired);
+
+ await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper("{}", typeInfo));
+ }
+
+ public class ClassWithRequiredField
+ {
+ public required int RequiredField;
+ }
+
+ [Fact]
+ public async Task RemovingPropertiesWithRequiredKeywordAllowsDeserialization()
+ {
+ JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+ {
+ for (int i = 0; i < ti.Properties.Count; i++)
+ {
+ if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
+ {
+ Assert.True(ti.Properties[i].IsRequired);
+ JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.FirstName));
+ property.Get = (obj) => ((PersonWithRequiredMembers)obj).FirstName;
+ property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).FirstName = (string)val;
+ ti.Properties[i] = property;
+ }
+ else if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.LastName))
+ {
+ Assert.True(ti.Properties[i].IsRequired);
+ JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.LastName));
+ property.Get = (obj) => ((PersonWithRequiredMembers)obj).LastName;
+ property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).LastName = (string)val;
+ ti.Properties[i] = property;
+ }
+ else
+ {
+ Assert.False(ti.Properties[i].IsRequired);
+ }
+ }
+ });
+
+ var obj = new PersonWithRequiredMembers()
+ {
+ FirstName = "foo",
+ LastName = "bar"
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+ json = """{"LastName":"bar"}""";
+ PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+ Assert.Null(deserialized.FirstName);
+ Assert.Equal("", deserialized.MiddleName);
+ Assert.Equal("bar", deserialized.LastName);
+ }
+
+ [Fact]
+ public async Task ChangingPropertiesWithRequiredKeywordToNotBeRequiredAllowsDeserialization()
+ {
+ JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+ {
+ for (int i = 0; i < ti.Properties.Count; i++)
+ {
+ ti.Properties[i].IsRequired = false;
+ }
+ });
+
+ var obj = new PersonWithRequiredMembers()
+ {
+ FirstName = "foo",
+ LastName = "bar"
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+ json = """{"LastName":"bar"}""";
+ PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+ Assert.Null(deserialized.FirstName);
+ Assert.Equal("", deserialized.MiddleName);
+ Assert.Equal("bar", deserialized.LastName);
+ }
+
+ [Fact]
+ public async Task RequiredNonDeserializablePropertyThrows()
+ {
+ JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+ {
+ for (int i = 0; i < ti.Properties.Count; i++)
+ {
+ if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
+ {
+ ti.Properties[i].Set = null;
+ }
+ }
+ });
+
+ string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar"}""";
+ InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+ Assert.Contains(nameof(PersonWithRequiredMembers.FirstName), exception.Message);
+ }
+
+ [Fact]
+ public async Task RequiredInitOnlyPropertyDoesNotThrow()
+ {
+ string json = """{"Prop":"foo"}""";
+ ClassWithInitOnlyRequiredProperty deserialized = await Serializer.DeserializeWrapper<ClassWithInitOnlyRequiredProperty>(json);
+ Assert.Equal("foo", deserialized.Prop);
+ }
+
+ public class ClassWithInitOnlyRequiredProperty
+ {
+ public required string Prop { get; init; }
+ }
+
+ [Fact]
+ public async Task RequiredExtensionDataPropertyThrows()
+ {
+ string json = """{"Foo":"foo","Bar":"bar"}""";
+ InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
+ async () => await Serializer.DeserializeWrapper<ClassWithRequiredExtensionDataProperty>(json));
+ Assert.Contains(nameof(ClassWithRequiredExtensionDataProperty.TestExtensionData), exception.Message);
+ }
+
+ public class ClassWithRequiredExtensionDataProperty
+ {
+ [JsonExtensionData]
+ public required Dictionary<string, JsonElement>? TestExtensionData { get; set; }
+ }
+
+ [Fact]
+ public async Task RequiredKeywordAndJsonRequiredCustomAttributeWorkCorrectlyTogether()
+ {
+ JsonSerializerOptions options = JsonSerializerOptions.Default;
+ JsonTypeInfo typeInfo = GetTypeInfo<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(options);
+ AssertJsonTypeInfoHasRequiredProperties(typeInfo,
+ nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty));
+
+ ClassWithRequiredKeywordAndJsonRequiredCustomAttribute obj = new()
+ {
+ SomeProperty = "foo"
+ };
+
+ string json = await Serializer.SerializeWrapper(obj, options);
+ Assert.Equal("""{"SomeProperty":"foo"}""", json);
+
+ var deserialized = await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options);
+ Assert.Equal(obj.SomeProperty, deserialized.SomeProperty);
+
+ json = "{}";
+ JsonException exception = await Assert.ThrowsAsync<JsonException>(
+ async () => await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options));
+
+ Assert.Contains(nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty), exception.Message);
+ }
+
+ public class ClassWithRequiredKeywordAndJsonRequiredCustomAttribute
+ {
+ [JsonRequired]
+ public required string SomeProperty { get; set; }
+ }
+
+ private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
+ {
+ options.TypeInfoResolver ??= JsonSerializerOptions.Default.TypeInfoResolver;
+ options.MakeReadOnly();
+ return options.GetTypeInfo(typeof(T));
+ }
+
+ private static void AssertJsonTypeInfoHasRequiredProperties(JsonTypeInfo typeInfo, params string[] requiredProperties)
+ {
+ HashSet<string> requiredPropertiesSet = new(requiredProperties);
+
+ foreach (var property in typeInfo.Properties)
+ {
+ if (requiredPropertiesSet.Remove(property.Name))
+ {
+ Assert.True(property.IsRequired);
+ }
+ else
+ {
+ Assert.False(property.IsRequired);
+ }
+ }
+
+ Assert.Empty(requiredPropertiesSet);
+ }
+ }
+}
<Import Project="System.Text.Json.TestLibrary.targets" />
<ItemGroup>
- <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+ <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
public sealed class CollectionTests_Metadata_String : CollectionTests_Metadata
{
public CollectionTests_Metadata_String()
- : base(new StringSerializerWrapper(CollectionTestsContext_Metadata.Default, (options) => new CollectionTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(CollectionTestsContext_Metadata.Default))
{
}
}
public sealed class CollectionTests_Metadata_AsyncStream : CollectionTests_Metadata
{
public CollectionTests_Metadata_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(CollectionTestsContext_Metadata.Default, (options) => new CollectionTestsContext_Metadata(options)))
+ : base(new AsyncStreamSerializerWrapper(CollectionTestsContext_Metadata.Default))
{
}
}
public partial class CollectionTests_Default : CollectionTests
{
public CollectionTests_Default()
- : base(new StringSerializerWrapper(CollectionTestsContext_Default.Default, (options) => new CollectionTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(CollectionTestsContext_Default.Default))
{
}
public sealed class ConstructorTests_Metadata_String : ConstructorTests_Metadata
{
public ConstructorTests_Metadata_String()
- : base(new StringSerializerWrapper(ConstructorTestsContext_Metadata.Default, (options) => new ConstructorTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(ConstructorTestsContext_Metadata.Default))
{
}
}
public sealed class ConstructorTests_Metadata_AsyncStream : ConstructorTests_Metadata
{
public ConstructorTests_Metadata_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Metadata.Default, (options) => new ConstructorTestsContext_Metadata(options)))
+ : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Metadata.Default))
{
}
}
public sealed class ConstructorTests_Default_String : ConstructorTests_Default
{
public ConstructorTests_Default_String()
- : base(new StringSerializerWrapper(ConstructorTestsContext_Default.Default, (options) => new ConstructorTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(ConstructorTestsContext_Default.Default))
{
}
}
public sealed class ConstructorTests_Default_AsyncStream : ConstructorTests_Default
{
public ConstructorTests_Default_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Default.Default, (options) => new ConstructorTestsContext_Default(options)))
+ : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Default.Default))
{
}
}
public sealed partial class ExtensionDataTests_Metadata : ExtensionDataTests
{
public ExtensionDataTests_Metadata()
- : base(new StringSerializerWrapper(ExtensionDataTestsContext_Metadata.Default, (options) => new ExtensionDataTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(ExtensionDataTestsContext_Metadata.Default))
{
}
public sealed partial class ExtensionDataTests_Default : ExtensionDataTests
{
public ExtensionDataTests_Default()
- : base(new StringSerializerWrapper(ExtensionDataTestsContext_Default.Default, (options) => new ExtensionDataTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(ExtensionDataTestsContext_Default.Default))
{
}
internal sealed class StringSerializerWrapper : JsonSerializerWrapper
{
private readonly JsonSerializerContext _defaultContext;
- private readonly Func<JsonSerializerOptions, JsonSerializerContext> _customContextCreator;
- public StringSerializerWrapper(JsonSerializerContext defaultContext, Func<JsonSerializerOptions, JsonSerializerContext> customContextCreator)
+ public StringSerializerWrapper(JsonSerializerContext defaultContext)
{
_defaultContext = defaultContext ?? throw new ArgumentNullException(nameof(defaultContext));
- _customContextCreator = customContextCreator ?? throw new ArgumentNullException(nameof(customContextCreator));
}
+ public override JsonSerializerOptions DefaultOptions => _defaultContext.Options;
+
public override Task<string> SerializeWrapper(object value, Type type, JsonSerializerOptions? options = null)
- {
- JsonSerializerContext context = GetJsonSerializerContext(options);
- return Task.FromResult(JsonSerializer.Serialize(value, type, context));
- }
+ => Task.FromResult(JsonSerializer.Serialize(value, type, GetOptions(options)));
public override Task<string> SerializeWrapper<T>(T value, JsonSerializerOptions? options = null)
- {
- Type runtimeType = GetRuntimeType(value);
-
- if (runtimeType != typeof(T))
- {
- return SerializeWrapper(value, runtimeType, options);
- }
-
- JsonSerializerContext context = GetJsonSerializerContext(options);
- JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
- return Task.FromResult(JsonSerializer.Serialize(value, typeInfo));
- }
+ => Task.FromResult(JsonSerializer.Serialize(value, GetOptions(options)));
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerContext context)
- => throw new NotImplementedException();
+ => Task.FromResult(JsonSerializer.Serialize(value, inputType, context));
public override Task<string> SerializeWrapper<T>(T value, JsonTypeInfo<T> jsonTypeInfo)
- => throw new NotImplementedException();
+ => Task.FromResult(JsonSerializer.Serialize(value, jsonTypeInfo));
public override Task<string> SerializeWrapper(object value, JsonTypeInfo jsonTypeInfo)
- => throw new NotImplementedException();
+ => Task.FromResult(JsonSerializer.Serialize(value, jsonTypeInfo));
public override Task<T> DeserializeWrapper<T>(string json, JsonSerializerOptions? options = null)
- {
- JsonSerializerContext context = GetJsonSerializerContext(options);
- JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
- return Task.FromResult(JsonSerializer.Deserialize<T>(json, typeInfo));
- }
+ => Task.FromResult(JsonSerializer.Deserialize<T>(json, GetOptions(options)));
public override Task<object> DeserializeWrapper(string json, Type type, JsonSerializerOptions? options = null)
- {
- JsonSerializerContext context = GetJsonSerializerContext(options);
- return Task.FromResult(JsonSerializer.Deserialize(json, type, context));
- }
+ => Task.FromResult(JsonSerializer.Deserialize(json, type, GetOptions(options)));
public override Task<T> DeserializeWrapper<T>(string json, JsonTypeInfo<T> jsonTypeInfo)
- => throw new NotImplementedException();
+ => Task.FromResult(JsonSerializer.Deserialize(json, jsonTypeInfo));
public override Task<object> DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo)
- => throw new NotImplementedException();
+ => Task.FromResult(JsonSerializer.Deserialize(json, jsonTypeInfo));
public override Task<object> DeserializeWrapper(string json, Type type, JsonSerializerContext context)
- => throw new NotImplementedException();
-
- private JsonSerializerContext GetJsonSerializerContext(JsonSerializerOptions? options)
- => options is null ? _defaultContext : _customContextCreator(new JsonSerializerOptions(options));
+ => Task.FromResult(JsonSerializer.Deserialize(json, type, context));
- private Type GetRuntimeType<TValue>(in TValue value)
+ private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null)
{
- if (typeof(TValue) == typeof(object) && value != null)
+ if (options is null)
+ {
+ return _defaultContext.Options;
+ }
+
+ if (options.TypeInfoResolver is null or DefaultJsonTypeInfoResolver { Modifiers.Count: 0 })
{
- return value.GetType();
+ return new JsonSerializerOptions(options) { TypeInfoResolver = _defaultContext };
}
- return typeof(TValue);
+ return options;
}
}
internal sealed class AsyncStreamSerializerWrapper : StreamingJsonSerializerWrapper
{
private readonly JsonSerializerContext _defaultContext;
- private readonly Func<JsonSerializerOptions, JsonSerializerContext> _customContextCreator;
+ public override JsonSerializerOptions DefaultOptions => _defaultContext.Options;
public override bool IsAsyncSerializer => true;
- public AsyncStreamSerializerWrapper(JsonSerializerContext defaultContext, Func<JsonSerializerOptions, JsonSerializerContext> customContextCreator)
+ public AsyncStreamSerializerWrapper(JsonSerializerContext defaultContext)
{
_defaultContext = defaultContext ?? throw new ArgumentNullException(nameof(defaultContext));
- _customContextCreator = customContextCreator ?? throw new ArgumentNullException(nameof(customContextCreator));
}
public override async Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonSerializerOptions? options = null)
{
- JsonSerializerContext context = GetJsonSerializerContext(options);
- JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
- return await JsonSerializer.DeserializeAsync<T>(utf8Json, typeInfo);
+ return await JsonSerializer.DeserializeAsync<T>(utf8Json, GetOptions(options));
}
public override async Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerOptions options = null)
{
- JsonSerializerContext context = GetJsonSerializerContext(options);
- return await JsonSerializer.DeserializeAsync(utf8Json, returnType, context);
+ return await JsonSerializer.DeserializeAsync(utf8Json, returnType, GetOptions(options));
}
- public override Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonTypeInfo<T> jsonTypeInfo) => throw new NotImplementedException();
- public override Task<object> DeserializeWrapper(Stream utf8Json, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException();
-
- public override async Task SerializeWrapper<T>(Stream stream, T value, JsonSerializerOptions options = null)
+ public override async Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonTypeInfo<T> jsonTypeInfo)
{
- Type runtimeType = GetRuntimeType(value);
- if (runtimeType != typeof(T))
- {
- await JsonSerializer.SerializeAsync(stream, value, runtimeType, options);
- return;
- }
+ return await JsonSerializer.DeserializeAsync<T>(utf8Json, jsonTypeInfo);
+ }
- JsonSerializerContext context = GetJsonSerializerContext(options);
- JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
- await JsonSerializer.SerializeAsync<T>(stream, value, typeInfo);
+ public override async Task<object> DeserializeWrapper(Stream utf8Json, JsonTypeInfo jsonTypeInfo)
+ {
+ return await JsonSerializer.DeserializeAsync(utf8Json, jsonTypeInfo);
}
- public override async Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerOptions options = null)
+ public override async Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerContext context)
{
- JsonSerializerContext context = GetJsonSerializerContext(options);
- await JsonSerializer.SerializeAsync(stream, value, inputType, context);
+ return await JsonSerializer.DeserializeAsync(utf8Json, returnType, context);
}
- public override Task SerializeWrapper<T>(Stream stream, T value, JsonTypeInfo<T> jsonTypeInfo) => throw new NotImplementedException();
- public override Task SerializeWrapper(Stream stream, object value, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException();
- public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerContext context) => throw new NotImplementedException();
- public override Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerContext context) => throw new NotImplementedException();
+ public override Task SerializeWrapper<T>(Stream stream, T value, JsonSerializerOptions options = null)
+ => JsonSerializer.SerializeAsync<T>(stream, value, GetOptions(options));
+
+ public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerOptions options = null)
+ => JsonSerializer.SerializeAsync(stream, value, inputType, GetOptions(options));
+
+ public override Task SerializeWrapper<T>(Stream stream, T value, JsonTypeInfo<T> jsonTypeInfo)
+ => JsonSerializer.SerializeAsync(stream, value, jsonTypeInfo);
+
+ public override Task SerializeWrapper(Stream stream, object value, JsonTypeInfo jsonTypeInfo)
+ => JsonSerializer.SerializeAsync(stream, value, jsonTypeInfo);
- private JsonSerializerContext GetJsonSerializerContext(JsonSerializerOptions? options)
- => options is null ? _defaultContext : _customContextCreator(new JsonSerializerOptions(options));
+ public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerContext context)
+ => JsonSerializer.SerializeAsync(stream, value, inputType, context);
- private Type GetRuntimeType<TValue>(in TValue value)
+ private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null)
{
- if (typeof(TValue) == typeof(object) && value != null)
+ if (options is null)
+ {
+ return _defaultContext.Options;
+ }
+
+ if (options.TypeInfoResolver is null or DefaultJsonTypeInfoResolver { Modifiers.Count: 0 })
{
- return value.GetType();
+ return new JsonSerializerOptions(options) { TypeInfoResolver = _defaultContext };
}
- return typeof(TValue);
+ return options;
}
}
}
public sealed partial class NodeInteropTests_Metadata : NodeInteropTests
{
public NodeInteropTests_Metadata()
- : base(new StringSerializerWrapper(NodeInteropTestsContext_Metadata.Default, (options) => new NodeInteropTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(NodeInteropTestsContext_Metadata.Default))
{
}
public sealed partial class NodeInteropTests_Default : NodeInteropTests
{
public NodeInteropTests_Default()
- : base(new StringSerializerWrapper(NodeInteropTestsContext_Default.Default, (options) => new NodeInteropTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(NodeInteropTestsContext_Default.Default))
{
}
public sealed partial class PropertyNameTests_Metadata : PropertyNameTests
{
public PropertyNameTests_Metadata()
- : base(new StringSerializerWrapper(PropertyNameTestsContext_Metadata.Default, (options) => new PropertyNameTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(PropertyNameTestsContext_Metadata.Default))
{
}
public sealed partial class PropertyNameTests_Default : PropertyNameTests
{
public PropertyNameTests_Default()
- : base(new StringSerializerWrapper(PropertyNameTestsContext_Default.Default, (options) => new PropertyNameTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(PropertyNameTestsContext_Default.Default))
{
}
public partial class PropertyVisibilityTests_Metadata : PropertyVisibilityTests
{
public PropertyVisibilityTests_Metadata()
- : this(new StringSerializerWrapper(PropertyVisibilityTestsContext_Metadata.Default, (options) => new PropertyVisibilityTestsContext_Metadata(options)))
+ : this(new StringSerializerWrapper(PropertyVisibilityTestsContext_Metadata.Default))
{
}
}
[Theory]
- [InlineData(typeof(ClassWithInitOnlyProperty))]
- [InlineData(typeof(StructWithInitOnlyProperty))]
- public override async Task InitOnlyProperties(Type type)
- {
- PropertyInfo property = type.GetProperty("MyInt");
-
- // Init-only properties can be serialized.
- object obj = Activator.CreateInstance(type);
- property.SetValue(obj, 1);
- Assert.Equal(@"{""MyInt"":1}", await Serializer.SerializeWrapper(obj, type));
-
- // Deserializing init-only properties is not supported.
- await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
- }
-
- [Theory]
[InlineData(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))]
[InlineData(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))]
[InlineData(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))]
public override async Task NonPublicInitOnlySetter_With_JsonInclude(Type type)
{
+ bool isDeserializationSupported = type == typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute);
+
PropertyInfo property = type.GetProperty("MyInt");
// Init-only properties can be serialized.
property.SetValue(obj, 1);
Assert.Equal(@"{""MyInt"":1}", await Serializer.SerializeWrapper(obj, type));
- // Deserializing init-only properties is not supported.
- await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
+ // Deserializing JsonInclude is only supported for internal properties
+ if (isDeserializationSupported)
+ {
+ obj = await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type);
+ Assert.Equal(1, (int)type.GetProperty("MyInt").GetValue(obj));
+ }
+ else
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
+ }
}
[Fact]
public partial class PropertyVisibilityTests_Default : PropertyVisibilityTests_Metadata
{
public PropertyVisibilityTests_Default()
- : base(new StringSerializerWrapper(PropertyVisibilityTestsContext_Default.Default, (options) => new PropertyVisibilityTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(PropertyVisibilityTestsContext_Default.Default))
{
}
public sealed class ReferenceHandlerTests_IgnoreCycles_Metadata_String : ReferenceHandlerTests_IgnoreCycles_Metadata
{
public ReferenceHandlerTests_IgnoreCycles_Metadata_String()
- : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Metadata(options)))
+ : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default))
{
}
}
public sealed class ReferenceHandlerTests_IgnoreCycles_Metadata_AsyncStream : ReferenceHandlerTests_IgnoreCycles_Metadata
{
public ReferenceHandlerTests_IgnoreCycles_Metadata_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Metadata(options)))
+ : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default))
{
}
}
public sealed class ReferenceHandlerTests_IgnoreCycles_Default_String : ReferenceHandlerTests_IgnoreCycles_Default
{
public ReferenceHandlerTests_IgnoreCycles_Default_String()
- : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Default(options)))
+ : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default))
{
}
}
public sealed class ReferenceHandlerTests_IgnoreCycles_Default_AsyncStream : ReferenceHandlerTests_IgnoreCycles_Default
{
public ReferenceHandlerTests_IgnoreCycles_Default_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Default(options)))
+ : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default))
{
}
}
public sealed class ReferenceHandlerTests_Metadata_String : ReferenceHandlerTests_Metadata
{
public ReferenceHandlerTests_Metadata_String()
- : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default, (options) => new ReferenceHandlerTestsContext_Metadata(options)))
+ : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default))
{
}
}
public sealed class ReferenceHandlerTests_Metadata_AsyncStream : ReferenceHandlerTests_Metadata
{
public ReferenceHandlerTests_Metadata_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default, (options) => new ReferenceHandlerTestsContext_Metadata(options)))
+ : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default))
{
}
}
public sealed class ReferenceHandlerTests_Default_String : ReferenceHandlerTests_Default
{
public ReferenceHandlerTests_Default_String()
- : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Default.Default, (options) => new ReferenceHandlerTestsContext_Default(options)))
+ : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Default.Default))
{
}
public sealed class ReferenceHandlerTests_Default_AsyncStream : ReferenceHandlerTests_Default
{
public ReferenceHandlerTests_Default_AsyncStream()
- : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Default.Default, (options) => new ReferenceHandlerTestsContext_Default(options)))
+ : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Default.Default))
{
}
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Json.Serialization.Tests;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.SourceGeneration.Tests
+{
+ public partial class RequiredKeywordTests_SourceGen : RequiredKeywordTests
+ {
+ public RequiredKeywordTests_SourceGen()
+ : base(new StringSerializerWrapper(RequiredKeywordTestsContext.Default))
+ {
+ }
+
+ [JsonSerializable(typeof(PersonWithRequiredMembers))]
+ [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtor))]
+ [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtor))]
+ [JsonSerializable(typeof(PersonWithRequiredMembersAndSetsRequiredMembers))]
+ [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers))]
+ [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers))]
+ [JsonSerializable(typeof(ClassWithInitOnlyRequiredProperty))]
+ [JsonSerializable(typeof(ClassWithRequiredField))]
+ [JsonSerializable(typeof(ClassWithRequiredExtensionDataProperty))]
+ [JsonSerializable(typeof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute))]
+ internal sealed partial class RequiredKeywordTestsContext : JsonSerializerContext
+ {
+ }
+ }
+}
public sealed partial class UnsupportedTypesTests_Metadata : UnsupportedTypesTests
{
public UnsupportedTypesTests_Metadata() : base(
- new StringSerializerWrapper(
- UnsupportedTypesTestsContext_Metadata.Default,
- (options) => new UnsupportedTypesTestsContext_Metadata(options)),
+ new StringSerializerWrapper(UnsupportedTypesTestsContext_Metadata.Default),
supportsJsonPathOnSerialize: true)
{
}
public sealed partial class UnsupportedTypesTests_Default : UnsupportedTypesTests
{
public UnsupportedTypesTests_Default() : base(
- new StringSerializerWrapper(
- UnsupportedTypesTestsContext_Default.Default,
- (options) => new UnsupportedTypesTestsContext_Default(options)),
+ new StringSerializerWrapper(UnsupportedTypesTestsContext_Default.Default),
supportsJsonPathOnSerialize: false)
{
}
<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <RoslynSupportsRequiredAttributes>true</RoslynSupportsRequiredAttributes>
+ </PropertyGroup>
+
<ItemGroup Condition="'$(ContinuousIntegrationBuild)' == 'true'">
<HighAotMemoryUsageAssembly Include="Microsoft.CodeAnalysis.CSharp.dll" />
- <HighAotMemoryUsageAssembly Include="System.Text.Json.SourceGeneration.Roslyn4.0.Tests.dll"/>
+ <HighAotMemoryUsageAssembly Include="System.Text.Json.SourceGeneration.Roslyn4.4.Tests.dll" />
</ItemGroup>
<Import Project="System.Text.Json.SourceGeneration.Tests.targets" />
<ItemGroup>
- <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
- <ProjectReference Include="..\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.0.csproj" />
+ <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" SkipGetTargetFrameworkProperties="true" />
+ <ProjectReference Include="..\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.4.csproj" />
</ItemGroup>
</Project>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CoreLibSharedDir)System\Runtime\Versioning\RequiresPreviewFeaturesAttribute.cs" Link="System\Runtime\Versioning\RequiresPreviewFeaturesAttribute.cs" />
+ <Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\CompilerFeatureRequiredAttribute.cs" Link="Common\System\Runtime\CompilerServices\CompilerFeatureRequiredAttribute.cs" />
+ <Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\RequiredMemberAttribute.cs" Link="Common\System\Runtime\CompilerServices\RequiredMemberAttribute.cs" />
+ <Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\SetsRequiredMembersAttribute.cs" Link="Common\System\Diagnostics\CodeAnalysis\SetsRequiredMembersAttribute.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="TestClasses.CustomConverters.cs" />
</ItemGroup>
+ <ItemGroup Condition="'$(RoslynSupportsRequiredAttributes)' == 'true'">
+ <Compile Include="..\Common\RequiredKeywordTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\RequiredKeywordTests.cs" />
+ <Compile Include="Serialization\RequiredKeywordTests.cs" />
+ </ItemGroup>
+
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<ProjectReference Include="..\..\src\System.Text.Json.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Collections.Immutable\src\System.Collections.Immutable.csproj" />
return CreateCompilation(source);
}
+ public static Compilation CreateCompilationWithRequiredProperties()
+ {
+ string source = @"
+ using System;
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ public class MyClass
+ {
+ public required string Required1 { get; set; }
+ public required string Required2 { get; set; }
+
+ public MyClass(string required1)
+ {
+ Required1 = required1;
+ }
+ }
+
+ [JsonSerializable(typeof(MyClass))]
+ public partial class MyJsonContext : JsonSerializerContext
+ {
+ }
+ }";
+
+ return CreateCompilation(source);
+ }
+
public static Compilation CreateCompilationWithRecordPositionalParameters()
{
string source = @"
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
}
[Fact]
- public void WarnOnClassesWithInitOnlyProperties()
+ public void DoNotWarnOnClassesWithInitOnlyProperties()
{
Compilation compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties();
JsonSourceGenerator generator = new JsonSourceGenerator();
CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
- Location location = compilation.GetSymbolsWithName("Id").First().Locations[0];
-
- (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
- {
- (location, "The type 'Location' defines init-only properties, deserialization of which is currently not supported in source generation mode.")
- };
-
CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, expectedWarningDiagnostics);
+ CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
}
}
[Fact]
- public void WarnOnClassesWithMixedInitOnlyProperties()
+ public void DoNotWarnOnClassesWithMixedInitOnlyProperties()
{
Compilation compilation = CompilationHelper.CreateCompilationWithMixedInitOnlyProperties();
JsonSourceGenerator generator = new JsonSourceGenerator();
CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
- Location location = compilation.GetSymbolsWithName("Orphaned").First().Locations[0];
-
- (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
- {
- (location, "The type 'MyClass' defines init-only properties, deserialization of which is currently not supported in source generation mode.")
- };
-
CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
- CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, expectedWarningDiagnostics);
+ CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
}
}
[Fact]
+ public void DoNotWarnOnClassesWithRequiredProperties()
+ {
+ Compilation compilation = CompilationHelper.CreateCompilationWithRequiredProperties();
+ JsonSourceGenerator generator = new JsonSourceGenerator();
+ Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
+
+ CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
+ CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
+ CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+ }
+
+ [Fact]
public void WarnOnClassesWithInaccessibleJsonIncludeProperties()
{
Compilation compilation = CompilationHelper.CreateCompilationWithInaccessibleJsonIncludeProperties();
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <RoslynApiVersion>$(MicrosoftCodeAnalysisVersion_4_0)</RoslynApiVersion>
- <DefineConstants>$(DefineConstants);ROSLYN4_0_OR_GREATER</DefineConstants>
+ <RoslynApiVersion>$(MicrosoftCodeAnalysisVersion_4_4)</RoslynApiVersion>
+ <DefineConstants>$(DefineConstants);ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER</DefineConstants>
<IsHighAotMemoryUsageTest>true</IsHighAotMemoryUsageTest>
</PropertyGroup>
<Import Project="System.Text.Json.SourceGeneration.Unit.Tests.targets" />
<ItemGroup>
- <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" />
+ <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" />
</ItemGroup>
</Project>
private class SpanSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override bool SupportsNullValueOnDeserialize => true; // a 'null' value is supported via implicit operator.
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
private class StringSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override bool SupportsNullValueOnDeserialize => true;
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
private readonly bool _forceSmallBufferInOptions;
private readonly bool _forceBomInsertions;
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override bool IsAsyncSerializer => true;
public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions;
private readonly bool _forceSmallBufferInOptions;
private readonly bool _forceBomInsertions;
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override bool IsAsyncSerializer => false;
public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions;
private class ReaderWriterSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
{
using MemoryStream stream = new MemoryStream();
private class DocumentSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
{
JsonDocument document = JsonSerializer.SerializeToDocument(value, inputType, options);
private class ElementSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
{
JsonElement element = JsonSerializer.SerializeToElement(value, inputType, options);
private class NodeSerializerWrapper : JsonSerializerWrapper
{
+ public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
public override bool SupportsNullValueOnDeserialize => true;
public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Text.Json.Serialization.Metadata;
-using Microsoft.Diagnostics.Runtime.ICorDebug;
-using Newtonsoft.Json;
using Xunit;
namespace System.Text.Json.Serialization.Tests
{
public RequiredKeywordTests_Node() : base(JsonSerializerWrapper.NodeSerializer) { }
}
-
- public abstract partial class RequiredKeywordTests : SerializerTests
- {
- public RequiredKeywordTests(JsonSerializerWrapper serializer) : base(serializer)
- {
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async void ClassWithRequiredKeywordDeserialization(bool ignoreNullValues)
- {
- JsonSerializerOptions options = new()
- {
- IgnoreNullValues = ignoreNullValues
- };
-
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembers>(options),
- nameof(PersonWithRequiredMembers.FirstName),
- nameof(PersonWithRequiredMembers.LastName));
-
- var obj = new PersonWithRequiredMembers()
- {
- FirstName = "foo",
- LastName = "bar"
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
- PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
- Assert.Equal(obj.FirstName, deserialized.FirstName);
- Assert.Equal(obj.MiddleName, deserialized.MiddleName);
- Assert.Equal(obj.LastName, deserialized.LastName);
-
- json = """{"LastName":"bar"}""";
- JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.DoesNotContain("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
-
- json = """{"LastName":null}""";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.DoesNotContain("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
-
- json = "{}";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.Contains("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
- }
-
- [Fact]
- public async void RequiredPropertyOccuringTwiceInThePayloadWorksAsExpected()
- {
- string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar","FirstName":"newfoo"}""";
- PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json);
- Assert.Equal("newfoo", deserialized.FirstName);
- Assert.Equal("", deserialized.MiddleName);
- Assert.Equal("bar", deserialized.LastName);
- }
-
- private class PersonWithRequiredMembers
- {
- public required string FirstName { get; set; }
- public string MiddleName { get; set; } = "";
- public required string LastName { get; set; }
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async void ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeserialization(bool ignoreNullValues)
- {
- JsonSerializerOptions options = new()
- {
- IgnoreNullValues = ignoreNullValues
- };
-
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtor>(options),
- nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.FirstName),
- nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.LastName),
- nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info1),
- nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info2));
-
- var obj = new PersonWithRequiredMembersAndSmallParametrizedCtor("badfoo", "badbar")
- {
- // note: these must be set during initialize or otherwise we get compiler errors
- FirstName = "foo",
- LastName = "bar",
- Info1 = "info1",
- Info2 = "info2",
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar","Info1":"info1","Info2":"info2"}""", json);
-
- var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
- Assert.Equal(obj.FirstName, deserialized.FirstName);
- Assert.Equal(obj.MiddleName, deserialized.MiddleName);
- Assert.Equal(obj.LastName, deserialized.LastName);
- Assert.Equal(obj.Info1, deserialized.Info1);
- Assert.Equal(obj.Info2, deserialized.Info2);
-
- json = """{"FirstName":"foo","MiddleName":"","LastName":null,"Info1":null,"Info2":"info2"}""";
- deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
- Assert.Equal(obj.FirstName, deserialized.FirstName);
- Assert.Equal(obj.MiddleName, deserialized.MiddleName);
- Assert.Null(deserialized.LastName);
- Assert.Null(deserialized.Info1);
- Assert.Equal(obj.Info2, deserialized.Info2);
-
- json = """{"LastName":"bar","Info1":"info1"}""";
- JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.DoesNotContain("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
- Assert.DoesNotContain("Info1", exception.Message);
- Assert.Contains("Info2", exception.Message);
-
- json = """{"LastName":null,"Info1":null}""";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.DoesNotContain("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
- Assert.DoesNotContain("Info1", exception.Message);
- Assert.Contains("Info2", exception.Message);
-
- json = "{}";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
- Assert.Contains("FirstName", exception.Message);
- Assert.Contains("LastName", exception.Message);
- Assert.DoesNotContain("MiddleName", exception.Message);
- Assert.Contains("Info1", exception.Message);
- Assert.Contains("Info2", exception.Message);
- }
-
- private class PersonWithRequiredMembersAndSmallParametrizedCtor
- {
- public required string FirstName { get; set; }
- public string MiddleName { get; set; } = "";
- public required string LastName { get; set; }
- public required string Info1 { get; set; }
- public required string Info2 { get; set; }
-
- public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName)
- {
- FirstName = firstName;
- LastName = lastName;
- }
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async void ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeserialization(bool ignoreNullValues)
- {
- JsonSerializerOptions options = new()
- {
- IgnoreNullValues = ignoreNullValues
- };
-
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtor>(options),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.AProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.BProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.CProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.DProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.EProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.FProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.GProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.HProp),
- nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.IProp));
-
- var obj = new PersonWithRequiredMembersAndLargeParametrizedCtor("bada", "badb", "badc", "badd", "bade", "badf", "badg")
- {
- // note: these must be set during initialize or otherwise we get compiler errors
- AProp = "a",
- BProp = "b",
- CProp = "c",
- DProp = "d",
- EProp = "e",
- FProp = "f",
- GProp = "g",
- HProp = "h",
- IProp = "i",
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","GProp":"g","HProp":"h","IProp":"i"}""", json);
-
- var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
- Assert.Equal(obj.AProp, deserialized.AProp);
- Assert.Equal(obj.BProp, deserialized.BProp);
- Assert.Equal(obj.CProp, deserialized.CProp);
- Assert.Equal(obj.DProp, deserialized.DProp);
- Assert.Equal(obj.EProp, deserialized.EProp);
- Assert.Equal(obj.FProp, deserialized.FProp);
- Assert.Equal(obj.GProp, deserialized.GProp);
- Assert.Equal(obj.HProp, deserialized.HProp);
- Assert.Equal(obj.IProp, deserialized.IProp);
-
- json = """{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":null,"FProp":"f","GProp":"g","HProp":null,"IProp":"i"}""";
- deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
- Assert.Equal(obj.AProp, deserialized.AProp);
- Assert.Equal(obj.BProp, deserialized.BProp);
- Assert.Equal(obj.CProp, deserialized.CProp);
- Assert.Equal(obj.DProp, deserialized.DProp);
- Assert.Null(deserialized.EProp);
- Assert.Equal(obj.FProp, deserialized.FProp);
- Assert.Equal(obj.GProp, deserialized.GProp);
- Assert.Null(deserialized.HProp);
- Assert.Equal(obj.IProp, deserialized.IProp);
-
- json = """{"AProp":"a","IProp":"i"}""";
- JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
- Assert.DoesNotContain("AProp", exception.Message);
- Assert.Contains("BProp", exception.Message);
- Assert.Contains("CProp", exception.Message);
- Assert.Contains("DProp", exception.Message);
- Assert.Contains("EProp", exception.Message);
- Assert.Contains("FProp", exception.Message);
- Assert.Contains("GProp", exception.Message);
- Assert.Contains("HProp", exception.Message);
- Assert.DoesNotContain("IProp", exception.Message);
-
- json = """{"AProp":null,"IProp":null}""";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
- Assert.DoesNotContain("AProp", exception.Message);
- Assert.Contains("BProp", exception.Message);
- Assert.Contains("CProp", exception.Message);
- Assert.Contains("DProp", exception.Message);
- Assert.Contains("EProp", exception.Message);
- Assert.Contains("FProp", exception.Message);
- Assert.Contains("GProp", exception.Message);
- Assert.Contains("HProp", exception.Message);
- Assert.DoesNotContain("IProp", exception.Message);
-
- json = """{"BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","HProp":"h"}""";
- exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
- Assert.Contains("AProp", exception.Message);
- Assert.DoesNotContain("BProp", exception.Message);
- Assert.DoesNotContain("CProp", exception.Message);
- Assert.DoesNotContain("DProp", exception.Message);
- Assert.DoesNotContain("EProp", exception.Message);
- Assert.DoesNotContain("FProp", exception.Message);
- Assert.Contains("GProp", exception.Message);
- Assert.DoesNotContain("HProp", exception.Message);
- Assert.Contains("IProp", exception.Message);
- }
-
- private class PersonWithRequiredMembersAndLargeParametrizedCtor
- {
- // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives
- public required string AProp { get; set; }
- public required string BProp { get; set; }
- public required string CProp { get; set; }
- public required string DProp { get; set; }
- public required string EProp { get; set; }
- public required string FProp { get; set; }
- public required string GProp { get; set; }
- public required string HProp { get; set; }
- public required string IProp { get; set; }
-
- public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop)
- {
- AProp = aprop;
- BProp = bprop;
- CProp = cprop;
- DProp = dprop;
- EProp = eprop;
- FProp = fprop;
- GProp = gprop;
- }
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async void ClassWithRequiredKeywordAndSetsRequiredMembersOnCtorWorks(bool useContext)
- {
- JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSetsRequiredMembers>(options)
- /* no required members */);
-
- var obj = new PersonWithRequiredMembersAndSetsRequiredMembers()
- {
- FirstName = "foo",
- LastName = "bar"
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
- json = """{"LastName":"bar"}""";
- var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSetsRequiredMembers>(json, options);
- Assert.Equal("", deserialized.FirstName);
- Assert.Equal("", deserialized.MiddleName);
- Assert.Equal("bar", deserialized.LastName);
- }
-
- private class PersonWithRequiredMembersAndSetsRequiredMembers
- {
- public required string FirstName { get; set; }
- public string MiddleName { get; set; } = "";
- public required string LastName { get; set; }
-
- [SetsRequiredMembers]
- public PersonWithRequiredMembersAndSetsRequiredMembers()
- {
- FirstName = "";
- LastName = "";
- }
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async void ClassWithRequiredKeywordSmallParametrizedCtorAndSetsRequiredMembersOnCtorWorks(bool useContext)
- {
- JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(options)
- /* no required members */);
-
- var obj = new PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers("foo", "bar");
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
- var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(json, options);
- Assert.Equal("foo", deserialized.FirstName);
- Assert.Equal("", deserialized.MiddleName);
- Assert.Equal("bar", deserialized.LastName);
- }
-
- private class PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers
- {
- public required string FirstName { get; set; }
- public string MiddleName { get; set; } = "";
- public required string LastName { get; set; }
-
- [SetsRequiredMembers]
- public PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers(string firstName, string lastName)
- {
- FirstName = firstName;
- LastName = lastName;
- }
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public async void ClassWithRequiredKeywordLargeParametrizedCtorAndSetsRequiredMembersOnCtorWorks(bool useContext)
- {
- JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
- AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(options)
- /* no required members */);
-
- var obj = new PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers("a", "b", "c", "d", "e", "f", "g");
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g"}""", json);
-
- var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(json, options);
- Assert.Equal("a", deserialized.A);
- Assert.Equal("b", deserialized.B);
- Assert.Equal("c", deserialized.C);
- Assert.Equal("d", deserialized.D);
- Assert.Equal("e", deserialized.E);
- Assert.Equal("f", deserialized.F);
- Assert.Equal("g", deserialized.G);
- }
-
- private class PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers
- {
- public required string A { get; set; }
- public required string B { get; set; }
- public required string C { get; set; }
- public required string D { get; set; }
- public required string E { get; set; }
- public required string F { get; set; }
- public required string G { get; set; }
-
- [SetsRequiredMembers]
- public PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers(string a, string b, string c, string d, string e, string f, string g)
- {
- A = a;
- B = b;
- C = c;
- D = d;
- E = e;
- F = f;
- G = g;
- }
- }
-
- [JsonSerializable(typeof(PersonWithRequiredMembersAndSetsRequiredMembers))]
- [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers))]
- [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers))]
- private partial class SetsRequiredMembersTestsContext : JsonSerializerContext { }
-
- [Fact]
- public async void RemovingPropertiesWithRequiredKeywordAllowsDeserialization()
- {
- JsonSerializerOptions options = new()
- {
- TypeInfoResolver = new DefaultJsonTypeInfoResolver()
- {
- Modifiers =
- {
- (ti) =>
- {
- for (int i = 0; i < ti.Properties.Count; i++)
- {
- if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
- {
- Assert.True(ti.Properties[i].IsRequired);
- JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.FirstName));
- property.Get = (obj) => ((PersonWithRequiredMembers)obj).FirstName;
- property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).FirstName = (string)val;
- ti.Properties[i] = property;
- }
- else if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.LastName))
- {
- Assert.True(ti.Properties[i].IsRequired);
- JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.LastName));
- property.Get = (obj) => ((PersonWithRequiredMembers)obj).LastName;
- property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).LastName = (string)val;
- ti.Properties[i] = property;
- }
- else
- {
- Assert.False(ti.Properties[i].IsRequired);
- }
- }
- }
- }
- }
- };
-
- var obj = new PersonWithRequiredMembers()
- {
- FirstName = "foo",
- LastName = "bar"
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
- json = """{"LastName":"bar"}""";
- PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
- Assert.Null(deserialized.FirstName);
- Assert.Equal("", deserialized.MiddleName);
- Assert.Equal("bar", deserialized.LastName);
- }
-
- [Fact]
- public async void ChangingPropertiesWithRequiredKeywordToNotBeRequiredAllowsDeserialization()
- {
- JsonSerializerOptions options = new()
- {
- TypeInfoResolver = new DefaultJsonTypeInfoResolver()
- {
- Modifiers =
- {
- (ti) =>
- {
- for (int i = 0; i < ti.Properties.Count; i++)
- {
- ti.Properties[i].IsRequired = false;
- }
- }
- }
- }
- };
-
- var obj = new PersonWithRequiredMembers()
- {
- FirstName = "foo",
- LastName = "bar"
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
- json = """{"LastName":"bar"}""";
- PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
- Assert.Null(deserialized.FirstName);
- Assert.Equal("", deserialized.MiddleName);
- Assert.Equal("bar", deserialized.LastName);
- }
-
- [Fact]
- public async void RequiredNonDeserializablePropertyThrows()
- {
- JsonSerializerOptions options = new()
- {
- TypeInfoResolver = new DefaultJsonTypeInfoResolver()
- {
- Modifiers =
- {
- (ti) =>
- {
- for (int i = 0; i < ti.Properties.Count; i++)
- {
- if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
- {
- ti.Properties[i].Set = null;
- }
- }
- }
- }
- }
- };
-
- string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar"}""";
- InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
- Assert.Contains(nameof(PersonWithRequiredMembers.FirstName), exception.Message);
- }
-
- [Fact]
- public async void RequiredInitOnlyPropertyDoesNotThrow()
- {
- string json = """{"Prop":"foo"}""";
- ClassWithInitOnlyRequiredProperty deserialized = await Serializer.DeserializeWrapper<ClassWithInitOnlyRequiredProperty>(json);
- Assert.Equal("foo", deserialized.Prop);
- }
-
- private class ClassWithInitOnlyRequiredProperty
- {
- public required string Prop { get; init; }
- }
-
- [Fact]
- public async void RequiredExtensionDataPropertyThrows()
- {
- string json = """{"Foo":"foo","Bar":"bar"}""";
- InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
- async () => await Serializer.DeserializeWrapper<ClassWithRequiredExtensionDataProperty>(json));
- Assert.Contains(nameof(ClassWithRequiredExtensionDataProperty.TestExtensionData), exception.Message);
- }
-
- private class ClassWithRequiredExtensionDataProperty
- {
- [JsonExtensionData]
- public required Dictionary<string, JsonElement>? TestExtensionData { get; set; }
- }
-
- [Fact]
- public async void RequiredKeywordAndJsonRequiredCustomAttributeWorkCorrectlyTogether()
- {
- JsonSerializerOptions options = JsonSerializerOptions.Default;
- JsonTypeInfo typeInfo = GetTypeInfo<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(options);
- AssertJsonTypeInfoHasRequiredProperties(typeInfo,
- nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty));
-
- ClassWithRequiredKeywordAndJsonRequiredCustomAttribute obj = new()
- {
- SomeProperty = "foo"
- };
-
- string json = await Serializer.SerializeWrapper(obj, options);
- Assert.Equal("""{"SomeProperty":"foo"}""", json);
-
- var deserialized = await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options);
- Assert.Equal(obj.SomeProperty, deserialized.SomeProperty);
-
- json = "{}";
- JsonException exception = await Assert.ThrowsAsync<JsonException>(
- async () => await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options));
-
- Assert.Contains(nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty), exception.Message);
- }
-
- private class ClassWithRequiredKeywordAndJsonRequiredCustomAttribute
- {
- [JsonRequired]
- public required string SomeProperty { get; set; }
- }
-
- private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
- {
- // For some variations of test (i.e. AsyncStreamWithSmallBuffer)
- // we don't use options directly and use copy with changed settings.
- // Because of that options.GetTypeInfo might throw even when Serializer.(De)SerializeWrapper was called..
- // We call into Serialize here to ensure that options are locked and options.GetTypeInfo queried.
- JsonSerializer.Serialize<T>(default(T), options);
- return options.GetTypeInfo(typeof(T));
- }
-
- private static void AssertJsonTypeInfoHasRequiredProperties(JsonTypeInfo typeInfo, params string[] requiredProperties)
- {
- HashSet<string> requiredPropertiesSet = new(requiredProperties);
-
- foreach (var property in typeInfo.Properties)
- {
- if (requiredPropertiesSet.Remove(property.Name))
- {
- Assert.True(property.IsRequired);
- }
- else
- {
- Assert.False(property.IsRequired);
- }
- }
-
- Assert.Empty(requiredPropertiesSet);
- }
- }
}
<Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.Deserialize.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.Deserialize.cs" />
<Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.IgnoreCycles.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.IgnoreCycles.cs" />
<Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.Serialize.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.Serialize.cs" />
+ <Compile Include="..\Common\RequiredKeywordTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\RequiredKeywordTests.cs" />
<Compile Include="..\Common\SampleTestData.OrderPayload.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\SampleTestData.OrderPayload.cs" />
<Compile Include="..\Common\SerializerTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\SerializerTests.cs" />
<Compile Include="..\Common\Utf8MemoryStream.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\Utf8MemoryStream.cs" />