How to json serialize custom/derived collection/list types? #124625
-
public class Track : List<Clip>
{
public string Name { get; set; }
}
public class Track
{
public string Name { get; set; }
public List<Clip> Items { get; set; }
}
public class Track : IEnumerable<Clip>
{
public string Name { get; set; }
public List<Clip> Items { get; set; }
} |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments
-
Beta Was this translation helpful? Give feedback.
-
|
Recommendation: Provide a {
"Name" : "default track",
"$Items" : [ ... ]
} |
Beta Was this translation helpful? Give feedback.
-
|
Currently, I am using using System.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace JsonObjectTest;
[JsonObject]
internal class MyList : List<int>
{
public Guid Id { get; set; }
[JsonProperty("NameProp")]
public string Name { get; set; } = "";
//[JsonRequired]
//[JsonProperty("$Items")]
//private int[] __items
//{
// get => [.. this];
// set
// {
// Clear();
// AddRange(value);
// }
//}
public MyList()
{
}
public MyList(IEnumerable<int> collection) : base(collection)
{
}
public MyList(int capacity) : base(capacity)
{
}
public MyList(Guid id, string name)
{
Id = id;
Name = name;
}
}
internal class Program
{
private static void Main(string[] args)
{
var list = new MyList {
0,
1,
2
};
var settings = new JsonSerializerSettings {
ContractResolver = new CollectionObjectResolver(),
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(list, settings);
Console.WriteLine(json);
var deserializedList = JsonConvert.DeserializeObject<MyList>(json, settings);
Console.WriteLine(deserializedList);
Console.WriteLine(JsonConvert.SerializeObject(deserializedList, settings));
}
}
#region CollectionObjectResolver
public sealed class CollectionObjectResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(
Type type,
MemberSerialization memberSerialization)
{
var props = base.CreateProperties(type, memberSerialization);
var systemCollectionBase = FindSystemCollectionBase(type);
if (systemCollectionBase is null)
{
return props;
}
props = props
.Where(p => p.DeclaringType != systemCollectionBase)
.ToList();
props.Add(CreateItemsProperty(type));
return props;
}
#region SystemCollectionBaseFinder
private static Type? FindSystemCollectionBase(Type type)
{
var current = type.BaseType;
while (current is not null && current != typeof(object))
{
if (typeof(IEnumerable).IsAssignableFrom(current) &&
current.Namespace?.StartsWith("System.") == true)
{
return current;
}
current = current.BaseType;
}
return null;
}
#endregion SystemCollectionBaseFinder
#region ItemsPropertyFactory
private static JsonProperty CreateItemsProperty(Type objectType)
{
var elementType = GetCollectionElementType(objectType)
?? throw new InvalidOperationException(
$"Cannot determine element type for {objectType}");
return new JsonProperty {
PropertyName = "$Items",
PropertyType = elementType.MakeArrayType(),
Readable = true,
Writable = true,
ValueProvider = new CollectionValueProvider(elementType)
};
}
private static Type? GetCollectionElementType(Type type)
{
if (type == typeof(string))
{
return null;
}
if (type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return type.GetGenericArguments()[0];
}
var enumerableInterface = type
.GetInterfaces()
.FirstOrDefault(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return enumerableInterface?.GetGenericArguments()[0];
}
#endregion ItemsPropertyFactory
}
#endregion CollectionObjectResolver
#region CollectionValueProvider
internal sealed class CollectionValueProvider : IValueProvider
{
private readonly Type _elementType;
public CollectionValueProvider(Type elementType)
{
_elementType = elementType;
}
public object? GetValue(object target)
{
if (target is not IEnumerable enumerable)
{
return null;
}
var items = enumerable.Cast<object?>().ToList();
var array = Array.CreateInstance(_elementType, items.Count);
for (int i = 0; i < items.Count; i++)
{
array.SetValue(items[i], i);
}
return array;
}
public void SetValue(object target, object? value)
{
if (value is not IEnumerable enumerable)
{
return;
}
if (target is not IList list)
{
return;
}
list.Clear();
foreach (object? item in enumerable)
{
list.Add(item);
}
}
}
#endregion CollectionValueProvider |
Beta Was this translation helpful? Give feedback.
-
|
var options = new JsonSerializerOptions
{
Converters = {
new CollectionObjectConverterFactory(
defaultItemsName: "$Items",
typeItemsNameMap: new Dictionary<Type, string>
{
{ typeof(TrackList), "$Tracks" },
{ typeof(Track), "$Clips" }
}
)
},
WriteIndented = true
};#region Factory
/// <summary>
/// A JsonConverterFactory that creates converters for collection types (classes implementing IEnumerable with a generic element type).
/// The converter serializes the collection as a JSON object containing all public properties plus a special property (configurable, default "$Items") that holds the collection items as an array.
/// </summary>
public sealed partial class CollectionObjectConverterFactory : JsonConverterFactory
{
/// <summary>
/// Regex pattern to validate items property names.
/// Valid names: optional '$' prefix followed by standard identifier (letters/underscore start, then alphanumeric/underscore).
/// </summary>
private static readonly Regex _validItemsNameRegex = ItemsNameRegex();
/// <summary>
/// Default items name used for collection types that don't have a specific mapping.
/// </summary>
private readonly string _defaultItemsName;
/// <summary>
/// Maps specific collection types to their custom items property names.
/// Enables different collection types to use different property names in JSON.
/// </summary>
private readonly Dictionary<Type, string> _typeItemsNameMap;
/// <summary>
/// Initializes a new instance of CollectionObjectConverterFactory with a default items name.
/// </summary>
/// <param name="defaultItemsName">Default items name for collection types. Default is "$Items".</param>
/// <exception cref="ArgumentException">Thrown when defaultItemsName is not a valid property name.</exception>
public CollectionObjectConverterFactory(string defaultItemsName = "$Items")
: this(defaultItemsName, [])
{
}
/// <summary>
/// Initializes a new instance of CollectionObjectConverterFactory with default and type-specific items names.
/// </summary>
/// <param name="defaultItemsName">Default items name for collection types. Default is "$Items".</param>
/// <param name="typeItemsNameMap">Dictionary mapping specific collection types to their custom items property names.</param>
/// <exception cref="ArgumentException">Thrown when any items name is not a valid property name.</exception>
public CollectionObjectConverterFactory(string defaultItemsName, Dictionary<Type, string> typeItemsNameMap)
{
if (!_validItemsNameRegex.IsMatch(defaultItemsName))
{
throw new ArgumentException(
$"The argument '{defaultItemsName}' is not a valid property name. Valid names consist of an optional '$' prefix followed by a standard identifier (letters or underscore followed by letters, digits, or underscores).",
nameof(defaultItemsName));
}
// Validate all custom items names in the mapping
foreach (var kvp in typeItemsNameMap)
{
if (!_validItemsNameRegex.IsMatch(kvp.Value))
{
throw new ArgumentException(
$"The items name '{kvp.Value}' for type '{kvp.Key.Name}' is not a valid property name. Valid names consist of an optional '$' prefix followed by a standard identifier (letters or underscore followed by letters, digits, or underscores).",
nameof(typeItemsNameMap));
}
}
_defaultItemsName = defaultItemsName;
_typeItemsNameMap = new Dictionary<Type, string>(typeItemsNameMap);
}
/// <summary>
/// Determines whether the specified type can be converted by this factory.
/// </summary>
/// <param name="typeToConvert">The type to check.</param>
/// <returns>True if the type is a class that implements IEnumerable and has a generic element type; otherwise, false.</returns>
public override bool CanConvert(Type typeToConvert)
{
// String is IEnumerable but should not be handled by this converter
if (typeToConvert == typeof(string))
{
return false;
}
// Only handle class types
if (!typeToConvert.IsClass)
{
return false;
}
// Type must implement IEnumerable
if (!typeof(IEnumerable).IsAssignableFrom(typeToConvert))
{
return false;
}
// Type must have a generic element type
return GetElementType(typeToConvert) is not null;
}
/// <summary>
/// Creates a JsonConverter instance for the specified collection type.
/// </summary>
/// <param name="typeToConvert">The collection type to convert.</param>
/// <param name="options">The JsonSerializerOptions.</param>
/// <returns>A JsonConverter instance configured for the specified type with its corresponding items name.</returns>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var elementType = GetElementType(typeToConvert)!;
var converterType = typeof(CollectionObjectConverter<,>)
.MakeGenericType(typeToConvert, elementType);
// Get the items name for this specific type, or use default
string itemsName = _typeItemsNameMap.TryGetValue(typeToConvert, out string? customName)
? customName
: _defaultItemsName;
return (JsonConverter)Activator.CreateInstance(converterType, options, itemsName)!;
}
/// <summary>
/// Extracts the generic element type from an IEnumerable implementation.
/// </summary>
/// <param name="type">The type to inspect.</param>
/// <returns>The generic element type if found; otherwise, null.</returns>
private static Type? GetElementType(Type type)
{
return type.GetInterfaces()
.FirstOrDefault(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
?.GetGenericArguments()[0];
}
[GeneratedRegex(@"^\$?[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled)]
private static partial Regex ItemsNameRegex();
}
#endregion Factory
#region Converter
public sealed class CollectionObjectConverter<TCollection, TElement> : JsonConverter<TCollection> where TCollection : class
{
/// <summary>
/// String comparer respecting the case-sensitivity setting from JsonSerializerOptions.
/// </summary>
private readonly StringComparer _comparer;
/// <summary>
/// The resolved constructor to use for creating instances.
/// Null indicates fallback to parameterless constructor.
/// </summary>
private readonly ConstructorInfo? _ctor;
/// <summary>
/// Index of the collection items parameter in the constructor, or -1 if not present.
/// Used to avoid duplicate population of collection items.
/// </summary>
private readonly int _ctorItemsParamIndex;
/// <summary>
/// Parameters of the resolved constructor.
/// </summary>
private readonly ParameterInfo[]? _ctorParams;
/// <summary>
/// Converter for deserializing individual collection elements.
/// </summary>
private readonly JsonConverter<TElement> _elementConverter;
/// <summary>
/// Configurable collection items property name (e.g., "$Items", "$Data", "items").
/// Can be different for different collection types when using type-specific configuration.
/// </summary>
private readonly string _itemsName;
/// <summary>
/// Lookup dictionary for properties by their JSON names.
/// Respects case-sensitivity setting and JsonPropertyName attributes.
/// </summary>
private readonly ReadOnlyDictionary<string, PropertyMeta> _lookup;
/// <summary>
/// The JsonSerializerOptions used for nested serialization/deserialization.
/// </summary>
private readonly JsonSerializerOptions _options;
/// <summary>
/// List of public readable properties (excluding indexed properties, system collection members, and ignored properties).
/// Ordered by JsonPropertyOrder attribute.
/// </summary>
private readonly ReadOnlyCollection<PropertyMeta> _properties;
/// <summary>
/// Initializes a new instance of CollectionObjectConverter.
/// </summary>
/// <param name="options">The JsonSerializerOptions.</param>
/// <param name="itemsName">The name of the property that holds collection items.</param>
public CollectionObjectConverter(JsonSerializerOptions options, string itemsName = "$Items")
{
_options = options;
_itemsName = itemsName;
_elementConverter = (JsonConverter<TElement>)options.GetConverter(typeof(TElement));
_comparer = options.PropertyNameCaseInsensitive
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal;
_properties = BuildPropertyMetas(options);
_lookup = _properties.ToDictionary(p => p.JsonName, _comparer).AsReadOnly();
(_ctor, _ctorParams, _ctorItemsParamIndex) = ResolveConstructor();
}
#region Read
/// <summary>
/// Deserializes a JSON object into a collection instance.
/// The JSON object should contain properties for the collection's public properties
/// and a special property (configurable, default "$Items") that contains the collection items as an array.
/// </summary>
/// <param name="reader">The JSON reader.</param>
/// <param name="typeToConvert">The type being converted (TCollection).</param>
/// <param name="options">The JsonSerializerOptions.</param>
/// <returns>The deserialized collection instance.</returns>
/// <exception cref="JsonException">Thrown if JSON structure is invalid or required properties are missing.</exception>
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
// Collect property values and items separately
var collectedValues = new Dictionary<string, object?>(_comparer);
var items = new List<TElement>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string name = reader.GetString()!;
reader.Read();
// Check if this is the collection items property
if (_comparer.Equals(name, _itemsName))
{
ReadItems(ref reader, items);
continue;
}
// Check if this property is a known property of the type
if (_lookup.TryGetValue(name, out var meta))
{
object? value = JsonSerializer.Deserialize(
ref reader,
meta.Property.PropertyType,
_options);
collectedValues[name] = value;
}
else
{
// Skip unknown properties
reader.Skip();
}
}
// Create the instance using resolved constructor and collected property values
var instance = CreateInstance(collectedValues, items);
// Only populate items through IList if the constructor did not accept a collection parameter
if (_ctorItemsParamIndex < 0)
{
if (instance is not IList list)
{
throw new JsonException($"Type '{typeof(TCollection).Name}' must implement 'IList' when the constructor does not accept collection items as a parameter.");
}
foreach (var item in items)
{
list.Add(item);
}
}
// Apply remaining properties that weren't used in the constructor
ApplyRemainingProperties(instance, collectedValues);
// Validate that all required properties were provided
ValidateRequired(collectedValues);
return instance;
}
/// <summary>
/// Deserializes a JSON array of items into the items list.
/// </summary>
/// <param name="reader">The JSON reader positioned at the start of the array.</param>
/// <param name="items">The list to populate with deserialized items.</param>
/// <exception cref="JsonException">Thrown if the JSON structure is invalid.</exception>
private void ReadItems(ref Utf8JsonReader reader, List<TElement> items)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var element = _elementConverter.Read(
ref reader,
typeof(TElement),
_options);
items.Add(element!);
}
}
#endregion Read
#region Write
/// <summary>
/// Serializes a collection instance into a JSON object.
/// The resulting JSON object contains all public properties as individual properties,
/// plus a special property (configurable, default "$Items") containing the collection items as an array.
/// </summary>
/// <param name="writer">The JSON writer.</param>
/// <param name="value">The collection instance to serialize.</param>
/// <param name="options">The JsonSerializerOptions.</param>
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
writer.WriteStartObject();
// Serialize all public properties
foreach (var meta in _properties)
{
writer.WritePropertyName(meta.JsonName);
object? val = meta.Property.GetValue(value);
JsonSerializer.Serialize(
writer,
val,
meta.Property.PropertyType,
_options);
}
// Serialize collection items with the configurable property name
writer.WritePropertyName(_itemsName);
writer.WriteStartArray();
foreach (var item in (IEnumerable<TElement>)value)
{
_elementConverter.Write(writer, item!, _options);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
#endregion Write
#region Constructor
/// <summary>
/// Creates an instance of the collection using the resolved constructor and provided values.
/// </summary>
/// <param name="values">Dictionary of property values keyed by their JSON names.</param>
/// <param name="items">List of collection items to be passed to the constructor or set via IList.</param>
/// <returns>A new instance of TCollection.</returns>
private TCollection CreateInstance(Dictionary<string, object?> values, List<TElement> items)
{
// If no suitable constructor was found, use the parameterless constructor
if (_ctor is null)
{
return Activator.CreateInstance<TCollection>()!;
}
// Prepare constructor arguments
object?[] args = new object?[_ctorParams!.Length];
for (int i = 0; i < _ctorParams.Length; i++)
{
// If this parameter corresponds to collection items, inject items directly
if (i == _ctorItemsParamIndex)
{
args[i] = items;
continue;
}
var p = _ctorParams[i];
// Find the corresponding property to get its JSON name
var property = _properties.FirstOrDefault(prop => _comparer.Equals(prop.Property.Name, p.Name));
if (property is not null)
{
// Look up the value using the property's JSON name
if (values.TryGetValue(property.JsonName, out object? val))
{
args[i] = val;
}
else
{
args[i] = null;
}
}
else
{
// Should not happen as ValidateConstructorParameters already checked this
args[i] = null;
}
}
return (TCollection)_ctor.Invoke(args)!;
}
/// <summary>
/// Resolves the constructor to use and infers the index of the collection items parameter.
/// </summary>
/// <remarks>
/// Matching rules:
/// 1. If [JsonConstructor] exists, it must be used. All parameters must match (either correspond to properties or be the collection items parameter).
/// 2. If no [JsonConstructor], select the constructor with the most parameters where all parameters match.
/// 3. If no valid constructor is found, return null (use parameterless constructor as fallback).
///
/// Collection parameter matching: Remove optional '$' prefix from _itemsName and compare with parameter name (case-insensitive).
/// For example, _itemsName = "$Data" → matches parameter "data"/"Data"/"DATA".
/// The collection parameter type must be assignable from List<TElement>.
/// </remarks>
private (ConstructorInfo?, ParameterInfo[]?, int) ResolveConstructor()
{
var ctors = typeof(TCollection)
.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (ctors.Length == 0)
{
return (null, null, -1);
}
// Look for constructor marked with [JsonConstructor]
var jsonCtor = ctors.FirstOrDefault(c => c.GetCustomAttribute<JsonConstructorAttribute>() is not null);
ConstructorInfo? selectedCtor;
if (jsonCtor is not null)
{
// If [JsonConstructor] is specified, it must be used, but all parameters must match
var (valid, itemsIndex, errorMsg) = ValidateConstructorParameters(jsonCtor.GetParameters());
if (!valid)
{
throw new JsonException($"The constructor marked with [JsonConstructor] on type '{typeof(TCollection).Name}' is invalid: {errorMsg}");
}
return (jsonCtor, jsonCtor.GetParameters(), itemsIndex);
}
// If no [JsonConstructor], iterate through all constructors and find the one with the most parameters where all parameters match
selectedCtor = null;
int bestItemsParamIndex = -1;
foreach (var ctor in ctors.OrderByDescending(c => c.GetParameters().Length))
{
var @params = ctor.GetParameters();
var (valid, itemsIndex, errorMsg) = ValidateConstructorParameters(@params);
if (valid)
{
selectedCtor = ctor;
bestItemsParamIndex = itemsIndex;
break;
}
}
if (selectedCtor is null)
{
// No valid constructor found, return null and use parameterless constructor as fallback
return (null, null, -1);
}
return (selectedCtor, selectedCtor.GetParameters(), bestItemsParamIndex);
}
/// <summary>
/// Validates whether all parameters of a constructor are valid.
/// Each parameter must either correspond to a CLR property name or be the collection items parameter.
/// </summary>
/// <returns>
/// (valid, itemsParamIndex, errorMessage):
/// - valid: Whether all parameters match successfully
/// - itemsParamIndex: Index of the collection items parameter (-1 if not present)
/// - errorMessage: Error description if validation fails (null if successful)
/// </returns>
private (bool, int, string?) ValidateConstructorParameters(ParameterInfo[] @params)
{
string bareItemsName = _itemsName.TrimStart('$');
int itemsParamIndex = -1;
for (int i = 0; i < @params.Length; i++)
{
string paramName = @params[i].Name!;
// First, check if it matches the collection items parameter (case-insensitive)
if (String.Equals(paramName, bareItemsName, StringComparison.OrdinalIgnoreCase))
{
// Collection parameter type must be assignable from List<TElement>
if (!@params[i].ParameterType.IsAssignableFrom(typeof(List<TElement>)))
{
string errorMsg = $"Parameter '{paramName}' of type '{@params[i].ParameterType.Name}' " +
$"cannot be assigned a value of type 'List<{typeof(TElement).Name}>'.";
return (false, -1, errorMsg);
}
itemsParamIndex = i;
continue;
}
// Then, check if it matches a CLR property name (case-insensitive)
var property = _properties.FirstOrDefault(p => String.Equals(p.Property.Name, paramName, StringComparison.OrdinalIgnoreCase));
if (property is null)
{
// Parameter matches neither a collection parameter nor any CLR property
string errorMsg = $"Parameter '{paramName}' does not correspond to any property " +
$"of type '{typeof(TCollection).Name}' or the collection items parameter '{bareItemsName}'.";
return (false, -1, errorMsg);
}
}
return (true, itemsParamIndex, null);
}
#endregion Constructor
#region Property Application
/// <summary>
/// Applies property values that were not consumed by the constructor to the instance.
/// </summary>
/// <param name="instance">The collection instance.</param>
/// <param name="values">Dictionary of property values keyed by their JSON names.</param>
private void ApplyRemainingProperties(TCollection instance, Dictionary<string, object?> values)
{
foreach (var (name, value) in values)
{
if (_lookup.TryGetValue(name, out var meta))
{
// Only set if the property has a setter
if (meta.Property.CanWrite)
{
meta.Property.SetValue(instance, value);
}
}
}
}
/// <summary>
/// Validates that all required properties have been provided in the JSON.
/// </summary>
/// <param name="values">Dictionary of property values keyed by their JSON names.</param>
/// <exception cref="JsonException">Thrown if any required properties are missing.</exception>
private void ValidateRequired(Dictionary<string, object?> values)
{
var missing = _properties
.Where(p => p.IsRequired && !values.ContainsKey(p.JsonName))
.Select(p => p.JsonName)
.ToList();
if (missing.Count > 0)
{
throw new JsonException($"One or more required properties are missing: {String.Join(", ", missing.Select(m => $"'{m}'"))}.");
}
}
#endregion Property Application
#region Metadata
/// <summary>
/// Builds property metadata for the collection type.
/// Includes public readable properties, ordered by JsonPropertyOrder attribute.
/// Excludes indexed properties, system collection members, and ignored properties.
/// </summary>
/// <param name="options">The JsonSerializerOptions.</param>
/// <returns>A list of property metadata ordered by JsonPropertyOrder.</returns>
private static ReadOnlyCollection<PropertyMeta> BuildPropertyMetas(JsonSerializerOptions options)
{
var list = new List<(PropertyMeta Meta, int Order)>();
foreach (var p in typeof(TCollection)
.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Skip indexed properties
if (p.GetIndexParameters().Length != 0)
{
continue;
}
// Skip write-only properties
if (!p.CanRead)
{
continue;
}
// Skip system collection members
if (IsSystemCollectionMember(p))
{
continue;
}
// Skip properties marked with [JsonIgnore] (except JsonIgnoreCondition.Never)
var ignore = p.GetCustomAttribute<JsonIgnoreAttribute>();
if (ignore is not null && ignore.Condition != JsonIgnoreCondition.Never)
{
continue;
}
string name = GetJsonName(p, options);
// Get the order from JsonPropertyOrder attribute, default to 0
int order =
p.GetCustomAttribute<JsonPropertyOrderAttribute>()?.Order ?? 0;
// Check if property is marked as required
bool required = p.GetCustomAttribute<JsonRequiredAttribute>() is not null;
list.Add((
new PropertyMeta {
JsonName = name,
Property = p,
IsRequired = required
},
order));
}
// Return properties ordered by JsonPropertyOrder
return list
.OrderBy(x => x.Order)
.Select(x => x.Meta)
.ToList()
.AsReadOnly();
}
/// <summary>
/// Gets the JSON name for a property, considering JsonPropertyName attribute and naming policy.
/// </summary>
/// <param name="p">The property to get the JSON name for.</param>
/// <param name="options">The JsonSerializerOptions containing the naming policy.</param>
/// <returns>The JSON name for the property.</returns>
private static string GetJsonName(PropertyInfo p, JsonSerializerOptions options)
{
// JsonPropertyName attribute takes precedence
var attr = p.GetCustomAttribute<JsonPropertyNameAttribute>();
return attr?.Name
?? options.PropertyNamingPolicy?.ConvertName(p.Name)
?? p.Name;
}
/// <summary>
/// Determines whether a property is a system collection member that should be excluded.
/// System collection properties like 'Count' on List types should not be serialized.
/// </summary>
/// <param name="p">The property to check.</param>
/// <returns>True if the property is a system collection member; otherwise, false.</returns>
private static bool IsSystemCollectionMember(PropertyInfo p)
{
var dt = p.DeclaringType;
if (dt is null)
{
return false;
}
// Only consider properties from types that implement IEnumerable as potential system members
if (!typeof(IEnumerable).IsAssignableFrom(dt))
{
return false;
}
// Filter out properties from System namespace (system collection implementations)
return dt.Namespace?.StartsWith("System.") is true;
}
#endregion Metadata
}
#endregion Converter
#region Meta
/// <summary>
/// Metadata about a property of the collection type.
/// Contains information needed for serialization and deserialization.
/// </summary>
internal sealed class PropertyMeta
{
/// <summary>
/// Indicates whether this property is required in the JSON.
/// Populated from [JsonRequired] attribute.
/// </summary>
public required bool IsRequired { get; init; }
/// <summary>
/// The JSON property name for this property.
/// Determined by [JsonPropertyName] attribute, naming policy, or the CLR property name.
/// </summary>
public required string JsonName { get; init; }
/// <summary>
/// The reflected PropertyInfo of the CLR property.
/// </summary>
public required PropertyInfo Property { get; init; }
}
#endregion Meta |
Beta Was this translation helpful? Give feedback.
System.Text.Jsonsolution: