File: System\Text\Json\Serialization\Metadata\JsonMetadataServices.Helpers.cs
Web Access
Project: src\runtime\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Reflection;
using System.Text.Json.Serialization.Converters;

namespace System.Text.Json.Serialization.Metadata
{
    public static partial class JsonMetadataServices
    {
        /// <summary>
        /// Creates serialization metadata for a type using a simple converter.
        /// </summary>
        private static JsonTypeInfo<T> CreateCore<T>(JsonConverter converter, JsonSerializerOptions options)
        {
            var typeInfo = new JsonTypeInfo<T>(converter, options);
            PopulatePolymorphismMetadata(typeInfo, polymorphismOptions: null, typeClassifierFactory: null);
            typeInfo.MapInterfaceTypesToCallbacks();

            // Plug in any converter configuration -- should be run last.
            converter.ConfigureJsonTypeInfo(typeInfo, options);
            typeInfo.IsCustomized = false;
            return typeInfo;
        }

        /// <summary>
        /// Creates serialization metadata for an object.
        /// </summary>
        private static JsonTypeInfo<T> CreateCore<T>(JsonSerializerOptions options, JsonObjectInfoValues<T> objectInfo)
        {
            JsonConverter<T> converter = GetConverter(objectInfo);
            var typeInfo = new JsonTypeInfo<T>(converter, options);
            if (objectInfo.ObjectWithParameterizedConstructorCreator != null)
            {
                // NB parameter metadata must be populated *before* property metadata
                // so that properties can be linked to their associated parameters.
                typeInfo.CreateObjectWithArgs = objectInfo.ObjectWithParameterizedConstructorCreator;
                PopulateParameterInfoValues(typeInfo, objectInfo.ConstructorParameterMetadataInitializer);
            }
            else
            {
                typeInfo.SetCreateObjectIfCompatible(objectInfo.ObjectCreator);
                typeInfo.CreateObjectForExtensionDataProperty = ((JsonTypeInfo)typeInfo).CreateObject;
            }

            if (objectInfo.PropertyMetadataInitializer != null)
            {
                typeInfo.SourceGenDelayedPropertyInitializer = objectInfo.PropertyMetadataInitializer;
            }
            else
            {
                typeInfo.PropertyMetadataSerializationNotSupported = true;
            }

            typeInfo.ConstructorAttributeProviderFactory = objectInfo.ConstructorAttributeProviderFactory;
            typeInfo.SerializeHandler = objectInfo.SerializeHandler;
            typeInfo.NumberHandling = objectInfo.NumberHandling;

            PopulatePolymorphismMetadata(typeInfo, objectInfo.PolymorphismOptions, objectInfo.TypeClassifierFactory);

            typeInfo.MapInterfaceTypesToCallbacks();

            // Plug in any converter configuration -- should be run last.
            converter.ConfigureJsonTypeInfo(typeInfo, options);
            typeInfo.IsCustomized = false;
            return typeInfo;
        }

        /// <summary>
        /// Creates serialization metadata for a collection.
        /// </summary>
        private static JsonTypeInfo<T> CreateCore<T>(
            JsonSerializerOptions options,
            JsonCollectionInfoValues<T> collectionInfo,
            JsonConverter<T> converter,
            object? createObjectWithArgs = null,
            object? addFunc = null)
        {
            ArgumentNullException.ThrowIfNull(collectionInfo);

            converter = collectionInfo.SerializeHandler != null
                ? new JsonMetadataServicesConverter<T>(converter)
                : converter;

            JsonTypeInfo<T> typeInfo = new JsonTypeInfo<T>(converter, options);

            typeInfo.KeyTypeInfo = collectionInfo.KeyInfo;
            typeInfo.ElementTypeInfo = collectionInfo.ElementInfo;
            Debug.Assert(typeInfo.Kind != JsonTypeInfoKind.None);
            typeInfo.NumberHandling = collectionInfo.NumberHandling;
            typeInfo.SerializeHandler = collectionInfo.SerializeHandler;
            typeInfo.CreateObjectWithArgs = createObjectWithArgs;
            typeInfo.AddMethodDelegate = addFunc;
            typeInfo.SetCreateObjectIfCompatible(collectionInfo.ObjectCreator);
            PopulatePolymorphismMetadata(typeInfo, collectionInfo.PolymorphismOptions, collectionInfo.TypeClassifierFactory);
            typeInfo.MapInterfaceTypesToCallbacks();

            // Plug in any converter configuration -- should be run last.
            converter.ConfigureJsonTypeInfo(typeInfo, options);
            typeInfo.IsCustomized = false;
            return typeInfo;
        }

        private static JsonConverter<T> GetConverter<T>(JsonObjectInfoValues<T> objectInfo)
        {
#pragma warning disable CS8714 // Nullability of type argument 'T' doesn't match 'notnull' constraint.
            JsonConverter<T> converter = objectInfo.ObjectWithParameterizedConstructorCreator != null
                ? new LargeObjectWithParameterizedConstructorConverter<T>()
                : new ObjectDefaultConverter<T>();
#pragma warning restore CS8714

            return objectInfo.SerializeHandler != null
                ? new JsonMetadataServicesConverter<T>(converter)
                : converter;
        }

        private static void PopulatePolymorphismMetadata(
            JsonTypeInfo typeInfo,
            JsonPolymorphismOptions? polymorphismOptions,
            JsonTypeClassifierFactory? typeClassifierFactory)
        {
            if (polymorphismOptions is null)
            {
                // Older versions of the source generator do not populate polymorphism options,
                // so attempt to populate it from attributes at runtime.
                polymorphismOptions = JsonPolymorphismOptions.CreateFromAttributeDeclarations(typeInfo.Type, out _);
                typeClassifierFactory = null;
            }
            else if (polymorphismOptions.IsEmpty)
            {
                // An empty options instance indicates no polymorphism metadata was provided at compile time.
                polymorphismOptions = null;
                typeClassifierFactory = null;
            }

            if (polymorphismOptions is not null)
            {
                typeInfo.SetPolymorphismOptions(polymorphismOptions);
                if (typeClassifierFactory is not null || typeInfo.Options.TypeClassifiers.Count > 0)
                {
                    typeInfo.TypeClassifierFactory = typeClassifierFactory;
                    typeInfo.TypeClassifierResolutionPending = true;
                }
            }
        }

        private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Func<JsonParameterInfoValues[]?>? paramFactory)
        {
            Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
            Debug.Assert(!typeInfo.IsReadOnly);

            if (paramFactory?.Invoke() is JsonParameterInfoValues[] parameterInfoValues)
            {
                typeInfo.PopulateParameterInfoValues(parameterInfoValues);
            }
            else
            {
                typeInfo.PropertyMetadataSerializationNotSupported = true;
            }
        }

        internal static void PopulateProperties(JsonTypeInfo typeInfo, JsonTypeInfo.JsonPropertyInfoList propertyList, Func<JsonSerializerContext, JsonPropertyInfo[]> propInitFunc)
        {
            Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
            Debug.Assert(!typeInfo.IsConfigured);
            Debug.Assert(typeInfo.Type != JsonTypeInfo.ObjectType);
            Debug.Assert(typeInfo.Converter.ElementType is null);

            JsonSerializerContext? context = typeInfo.Options.TypeInfoResolver as JsonSerializerContext;
            JsonPropertyInfo[] properties = propInitFunc(context!);

            // Regardless of the source generator we need to re-run the naming conflict resolution algorithm
            // at run time since it is possible that the naming policy or other configs can be different then.
            JsonTypeInfo.PropertyHierarchyResolutionState state = new(typeInfo.Options);

            foreach (JsonPropertyInfo jsonPropertyInfo in properties)
            {
                if (!jsonPropertyInfo.SrcGen_IsPublic)
                {
                    if (jsonPropertyInfo.SrcGen_HasJsonInclude)
                    {
                        if (jsonPropertyInfo.Get is null && jsonPropertyInfo.Set is null)
                        {
                            // [JsonInclude] property is inaccessible and the source generator
                            // did not provide getter/setter delegates (e.g. older generator).
                            Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName is not set by source gen");
                            ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnInaccessibleProperty(jsonPropertyInfo.MemberName, jsonPropertyInfo.DeclaringType);
                        }

                        // Non-public [JsonInclude] property with getter/setter delegates provided
                        // via UnsafeAccessor or reflection fallback — treat it as a valid property.
                    }
                    else
                    {
                        continue;
                    }
                }

                if (jsonPropertyInfo.MemberType == MemberTypes.Field && !jsonPropertyInfo.SrcGen_HasJsonInclude && !typeInfo.Options.IncludeFields)
                {
                    continue;
                }

                propertyList.AddPropertyWithConflictResolution(jsonPropertyInfo, ref state);
            }

            if (state.IsPropertyOrderSpecified)
            {
                propertyList.SortProperties();
            }
        }

        private static JsonPropertyInfo<T> CreatePropertyInfoCore<T>(JsonPropertyInfoValues<T> propertyInfoValues, JsonSerializerOptions options)
        {
            var propertyInfo = new JsonPropertyInfo<T>(propertyInfoValues.DeclaringType, declaringTypeInfo: null, options);

            DeterminePropertyName(propertyInfo,
                declaredPropertyName: propertyInfoValues.PropertyName,
                declaredJsonPropertyName: propertyInfoValues.JsonPropertyName);

            propertyInfo.MemberName = propertyInfoValues.PropertyName;
            propertyInfo.MemberType = propertyInfoValues.IsProperty ? MemberTypes.Property : MemberTypes.Field;
            propertyInfo.SrcGen_IsPublic = propertyInfoValues.IsPublic;
            propertyInfo.SrcGen_HasJsonInclude = propertyInfoValues.HasJsonInclude;
            propertyInfo.IsExtensionData = propertyInfoValues.IsExtensionData;
            propertyInfo.CustomConverter = propertyInfoValues.Converter;

            if (propertyInfo.IgnoreCondition != JsonIgnoreCondition.Always)
            {
                propertyInfo.Get = propertyInfoValues.Getter!;
                propertyInfo.Set = propertyInfoValues.Setter;
            }

            propertyInfo.IgnoreCondition = propertyInfoValues.IgnoreCondition;
            propertyInfo.JsonTypeInfo = propertyInfoValues.PropertyTypeInfo;
            propertyInfo.NumberHandling = propertyInfoValues.NumberHandling;
            propertyInfo.AttributeProviderFactory = propertyInfoValues.AttributeProviderFactory;

            return propertyInfo;
        }

        private static void DeterminePropertyName(
            JsonPropertyInfo propertyInfo,
            string declaredPropertyName,
            string? declaredJsonPropertyName)
        {
            string? name;

            // Property name settings.
            if (declaredJsonPropertyName != null)
            {
                name = declaredJsonPropertyName;
            }
            else if (propertyInfo.Options.PropertyNamingPolicy == null)
            {
                name = declaredPropertyName;
            }
            else
            {
                name = propertyInfo.Options.PropertyNamingPolicy.ConvertName(declaredPropertyName);
            }

            // Compat: We need to do validation before we assign Name so that we get InvalidOperationException rather than ArgumentNullException
            if (name == null)
            {
                ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(propertyInfo);
            }

            propertyInfo.Name = name;
        }
    }
}