Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,27 @@ internal void Configure()
}
else
{
_jsonTypeInfo ??= Options.GetTypeInfoInternal(PropertyType);
_jsonTypeInfo.EnsureConfigured();
// Try to expand any custom converter first to avoid
// eagerly resolving JsonTypeInfo for types that have custom converters.
// This prevents failures for types with properties that cannot be serialized
// (e.g., ref properties) when they have a custom converter that handles serialization.
JsonConverter? expandedCustomConverter = null;
if (CustomConverter is not null)
{
expandedCustomConverter = Options.ExpandConverterFactory(CustomConverter, PropertyType);
}

DetermineEffectiveConverter(_jsonTypeInfo);
if (expandedCustomConverter is null)
{
// If expandedCustomConverter is null, it means either:
// (1) no custom converter was specified, or
// (2) a custom converter was specified but the factory returned null after expansion.
// In either case, we need to get the JsonTypeInfo.
_jsonTypeInfo ??= Options.GetTypeInfoInternal(PropertyType);
_jsonTypeInfo.EnsureConfigured();
}

DetermineEffectiveConverter(_jsonTypeInfo, expandedCustomConverter);
DetermineNumberHandlingForProperty();
DetermineEffectiveObjectCreationHandlingForProperty();
DetermineSerializationCapabilities();
Expand Down Expand Up @@ -466,7 +483,7 @@ internal void Configure()
IsConfigured = true;
}

private protected abstract void DetermineEffectiveConverter(JsonTypeInfo jsonTypeInfo);
private protected abstract void DetermineEffectiveConverter(JsonTypeInfo? jsonTypeInfo, JsonConverter? expandedCustomConverter);

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
Expand Down Expand Up @@ -592,14 +609,14 @@ private void DetermineNumberHandlingForProperty()
{
Debug.Assert(DeclaringTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo");
Debug.Assert(!IsConfigured, "Should not be called post-configuration.");
Debug.Assert(_jsonTypeInfo != null, "Must have already been determined on configuration.");

bool numberHandlingIsApplicable = NumberHandingIsApplicable();

if (numberHandlingIsApplicable)
{
// Priority 1: Get handling from attribute on property/field, its parent class type or property type.
JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeInfo.NumberHandling ?? _jsonTypeInfo.NumberHandling;
// _jsonTypeInfo may be null if using a custom converter that doesn't need type metadata.
JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeInfo.NumberHandling ?? _jsonTypeInfo?.NumberHandling;

// Priority 2: Get handling from JsonSerializerOptions instance.
if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict)
Expand Down Expand Up @@ -659,9 +676,12 @@ private void DetermineEffectiveObjectCreationHandlingForProperty()
ThrowHelper.ThrowInvalidOperationException_ObjectCreationHandlingPropertyValueTypeMustHaveASetter(this);
}

Debug.Assert(_jsonTypeInfo != null);
Debug.Assert(_jsonTypeInfo.IsConfigurationStarted);
if (JsonTypeInfo.SupportsPolymorphicDeserialization)
// _jsonTypeInfo may be null if using a custom converter that doesn't need type metadata.
if (_jsonTypeInfo is not null)
{
Debug.Assert(_jsonTypeInfo.IsConfigurationStarted);
}
if (JsonTypeInfo?.SupportsPolymorphicDeserialization == true)
{
ThrowHelper.ThrowInvalidOperationException_ObjectCreationHandlingPropertyCannotAllowPolymorphicDeserialization(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,24 @@ internal override void AddJsonParameterInfo(JsonParameterInfoValues parameterInf
internal override void DetermineReflectionPropertyAccessors(MemberInfo memberInfo, bool useNonPublicAccessors)
=> DefaultJsonTypeInfoResolver.DeterminePropertyAccessors<T>(this, memberInfo, useNonPublicAccessors);

private protected override void DetermineEffectiveConverter(JsonTypeInfo jsonTypeInfo)
private protected override void DetermineEffectiveConverter(JsonTypeInfo? jsonTypeInfo, JsonConverter? expandedCustomConverter)
{
Debug.Assert(jsonTypeInfo is JsonTypeInfo<T>);
Debug.Assert(jsonTypeInfo is null or JsonTypeInfo<T>);

JsonConverter<T> converter =
Options.ExpandConverterFactory(CustomConverter, PropertyType) // Expand any property-level custom converters.
?.CreateCastingConverter<T>() // Cast to JsonConverter<T>, potentially with wrapping.
?? ((JsonTypeInfo<T>)jsonTypeInfo).EffectiveConverter; // Fall back to the effective converter for the type.
JsonConverter<T> converter;
if (expandedCustomConverter is not null)
{
// Use the already-expanded custom converter
converter = expandedCustomConverter.CreateCastingConverter<T>();
}
else
{
// jsonTypeInfo should have been provided by caller when no custom converter is available
Debug.Assert(jsonTypeInfo is not null, "JsonTypeInfo must be provided when custom converter is not available.");

// Fall back to the effective converter for the type
converter = ((JsonTypeInfo<T>)jsonTypeInfo).EffectiveConverter;
}

_effectiveConverter = converter;
_typedEffectiveConverter = converter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,16 +763,21 @@ private void Configure()
}
}

if (ElementType != null)
// Only resolve element and key types if using built-in converters.
// Custom converters should handle their element/key types themselves.
if (Converter.IsInternalConverter)
{
_elementTypeInfo ??= Options.GetTypeInfoInternal(ElementType);
_elementTypeInfo.EnsureConfigured();
}
if (ElementType != null)
{
_elementTypeInfo ??= Options.GetTypeInfoInternal(ElementType);
_elementTypeInfo.EnsureConfigured();
}

if (KeyType != null)
{
_keyTypeInfo ??= Options.GetTypeInfoInternal(KeyType);
_keyTypeInfo.EnsureConfigured();
if (KeyType != null)
{
_keyTypeInfo ??= Options.GetTypeInfoInternal(KeyType);
_keyTypeInfo.EnsureConfigured();
}
}

DetermineIsCompatibleWithCurrentOptions();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// 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.Text.Json.Serialization;
using Xunit;

namespace System.Text.Json.SourceGeneration.Tests
{
public static class InvalidTypesWithConvertersTests
{
[Fact]
public static void SourceGen_CustomConverter_OnProperty_WithInvalidElementType_Succeeds()
{
var collection = new SourceGenCollectionWithInvalidElementType
{
Items = new List<SourceGenTypeWithRefProperty>
{
new SourceGenTypeWithRefProperty { Value1 = 42 }
}
};

string json = JsonSerializer.Serialize(collection, InvalidTypesContext.Default.SourceGenCollectionWithInvalidElementType);
Assert.Equal(@"{""Items"":[]}", json);

var deserialized = JsonSerializer.Deserialize(json, InvalidTypesContext.Default.SourceGenCollectionWithInvalidElementType);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Items);
Assert.Empty(deserialized.Items);
}

[Fact]
public static void SourceGen_CustomConverter_OnProperty_WithInvalidType_Succeeds()
{
var obj = new SourceGenClassWithInvalidTypeProperty
{
InvalidProperty = new SourceGenTypeWithRefProperty { Value1 = 100 }
};

string json = JsonSerializer.Serialize(obj, InvalidTypesContext.Default.SourceGenClassWithInvalidTypeProperty);
Assert.Equal(@"{""InvalidProperty"":""custom""}", json);

var deserialized = JsonSerializer.Deserialize(json, InvalidTypesContext.Default.SourceGenClassWithInvalidTypeProperty);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.InvalidProperty);
Assert.Equal(999, deserialized.InvalidProperty.Value1);
}

[Fact]
public static void SourceGen_CustomConverter_OnType_WithRefProperty_Succeeds()
{
var obj = new SourceGenClassWithTypeConverterAttribute
{
Item = new SourceGenTypeWithRefPropertyAndTypeConverter { Value1 = 50 }
};

string json = JsonSerializer.Serialize(obj, InvalidTypesContext.Default.SourceGenClassWithTypeConverterAttribute);
Assert.Equal(@"{""Item"":""type-converter""}", json);

var deserialized = JsonSerializer.Deserialize(json, InvalidTypesContext.Default.SourceGenClassWithTypeConverterAttribute);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Item);
Assert.Equal(888, deserialized.Item.Value1);
}

[Fact]
public static void SourceGen_CustomConverter_OnDictionary_WithInvalidValueType_Succeeds()
{
var obj = new SourceGenClassWithDictionaryOfInvalidType
{
Items = new Dictionary<string, SourceGenTypeWithRefProperty>
{
["key1"] = new SourceGenTypeWithRefProperty { Value1 = 1 }
}
};

string json = JsonSerializer.Serialize(obj, InvalidTypesContext.Default.SourceGenClassWithDictionaryOfInvalidType);
Assert.Equal(@"{""Items"":{}}", json);

var deserialized = JsonSerializer.Deserialize(json, InvalidTypesContext.Default.SourceGenClassWithDictionaryOfInvalidType);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Items);
Assert.Empty(deserialized.Items);
}
}

// Test classes for source generation

public class SourceGenCollectionWithInvalidElementType
{
[JsonConverter(typeof(SourceGenListOfInvalidTypeConverter))]
public IList<SourceGenTypeWithRefProperty> Items { get; set; }
}

public class SourceGenClassWithInvalidTypeProperty
{
[JsonConverter(typeof(SourceGenInvalidTypeConverter))]
public SourceGenTypeWithRefProperty InvalidProperty { get; set; }
}

public class SourceGenClassWithTypeConverterAttribute
{
public SourceGenTypeWithRefPropertyAndTypeConverter Item { get; set; }
}

public class SourceGenClassWithDictionaryOfInvalidType
{
[JsonConverter(typeof(SourceGenDictionaryOfInvalidTypeConverter))]
public IDictionary<string, SourceGenTypeWithRefProperty> Items { get; set; }
}

// Type with ref property - invalid for serialization
public class SourceGenTypeWithRefProperty
{
public int Value1 { get; set; }

private int _value2;
public ref int Value2 => ref _value2;
}

// Type with ref property and [JsonConverter] attribute on the type itself
[JsonConverter(typeof(SourceGenTypeWithRefPropertyConverter))]
public class SourceGenTypeWithRefPropertyAndTypeConverter
{
public int Value1 { get; set; }

private int _value2;
public ref int Value2 => ref _value2;
}

// Custom converter for IList<SourceGenTypeWithRefProperty>
public class SourceGenListOfInvalidTypeConverter : JsonConverter<IList<SourceGenTypeWithRefProperty>>
{
public override IList<SourceGenTypeWithRefProperty> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
reader.Skip();
return new List<SourceGenTypeWithRefProperty>();
}

public override void Write(Utf8JsonWriter writer, IList<SourceGenTypeWithRefProperty> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteEndArray();
}
}

// Custom converter for SourceGenTypeWithRefProperty
public class SourceGenInvalidTypeConverter : JsonConverter<SourceGenTypeWithRefProperty>
{
public override SourceGenTypeWithRefProperty Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
reader.Skip();
return new SourceGenTypeWithRefProperty { Value1 = 999 };
}

public override void Write(Utf8JsonWriter writer, SourceGenTypeWithRefProperty value, JsonSerializerOptions options)
{
writer.WriteStringValue("custom");
}
}

// Custom converter for SourceGenTypeWithRefPropertyAndTypeConverter
public class SourceGenTypeWithRefPropertyConverter : JsonConverter<SourceGenTypeWithRefPropertyAndTypeConverter>
{
public override SourceGenTypeWithRefPropertyAndTypeConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
reader.Skip();
return new SourceGenTypeWithRefPropertyAndTypeConverter { Value1 = 888 };
}

public override void Write(Utf8JsonWriter writer, SourceGenTypeWithRefPropertyAndTypeConverter value, JsonSerializerOptions options)
{
writer.WriteStringValue("type-converter");
}
}

// Custom converter for IDictionary<string, SourceGenTypeWithRefProperty>
public class SourceGenDictionaryOfInvalidTypeConverter : JsonConverter<IDictionary<string, SourceGenTypeWithRefProperty>>
{
public override IDictionary<string, SourceGenTypeWithRefProperty> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
reader.Skip();
return new Dictionary<string, SourceGenTypeWithRefProperty>();
}

public override void Write(Utf8JsonWriter writer, IDictionary<string, SourceGenTypeWithRefProperty> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteEndObject();
}
}

// Source generation context
[JsonSerializable(typeof(SourceGenCollectionWithInvalidElementType))]
[JsonSerializable(typeof(SourceGenClassWithInvalidTypeProperty))]
[JsonSerializable(typeof(SourceGenClassWithTypeConverterAttribute))]
[JsonSerializable(typeof(SourceGenClassWithDictionaryOfInvalidType))]
internal partial class InvalidTypesContext : JsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<Compile Include="SerializationLogicTests.cs" />
<Compile Include="TestClasses.cs" />
<Compile Include="TestClasses.CustomConverters.cs" />
<Compile Include="InvalidTypesWithConvertersTests.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TestedRoslynVersion)' >= '4.0'">
Expand Down
Loading
Loading