< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 106
Coverable lines: 106
Total lines: 194
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor()100%110%
GetDefaultConverterStrategy()100%110%
OnTryRead(...)0%14140%
TryResolveCaseType(...)0%12120%
GetJsonValueType(...)0%12120%
OnTryWrite(...)0%10100%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Union/JsonUnionConverter.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Text.Json.Serialization.Metadata;
 5
 6namespace System.Text.Json.Serialization.Converters
 7{
 8    /// <summary>
 9    /// Converter for union types. Fully stateless — reads/writes using the classifier,
 10    /// deconstructor, and constructor delegates configured on <see cref="JsonTypeInfo{T}"/>.
 11    /// All configuration is performed by the resolver, not the converter.
 12    /// </summary>
 13    internal sealed class JsonUnionConverter<TUnion> : JsonResumableConverter<TUnion>
 14    {
 015        public JsonUnionConverter()
 016        {
 17            // The union converter dispatches across all JSON value shapes (the actual case
 18            // type determines which shape is valid). The non-Value converter machinery
 19            // verifies post-Read positioning per token kind; flagging SupportsMultipleTokenTypes
 20            // lets value-token cases (string/number/bool) satisfy the verification.
 021            SupportsMultipleTokenTypes = true;
 022        }
 23
 24        // Override the framework's default null short-circuit so that JSON null
 25        // is delivered to Read where it is dispatched to the case constructor
 26        // accepting null (or rejected when no case opts in).
 027        public override bool HandleNull => true;
 28
 029        private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Union;
 30
 31        internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, s
 032        {
 033            JsonTypeInfo<TUnion> typeInfo = (JsonTypeInfo<TUnion>)state.Current.JsonTypeInfo;
 34
 035            Func<Type, object?, TUnion>? constructor = typeInfo.UnionConstructor;
 036            if (constructor is null)
 037            {
 038                ThrowHelper.ThrowJsonException_UnionCannotCreateValue(typeToConvert);
 39            }
 40
 041            if (reader.TokenType is JsonTokenType.Null)
 042            {
 043                Type? nullableCaseType = typeInfo.UnionNullableCaseType;
 044                if (nullableCaseType is null)
 045                {
 46                    // Round-trip symmetry with the serializer: default(union) writes
 47                    // JSON null (deconstructor returns (null, null)), so JSON null must
 48                    // read back as default(union). For value-type unions this is the
 49                    // zero-init struct; for reference-type unions this is null.
 50                    // See https://github.com/dotnet/runtime/issues/128834.
 051                    value = default;
 052                    return true;
 53                }
 54
 055                value = constructor(nullableCaseType, null);
 056                return true;
 57            }
 58
 59            JsonTypeInfo caseTypeInfo;
 60            Type caseType;
 61
 062            if (state.IsContinuation)
 063            {
 064                caseTypeInfo = state.Current.JsonPropertyInfo!.JsonTypeInfo;
 065                caseType = caseTypeInfo.Type;
 066            }
 67            else
 068            {
 069                if (!TryResolveCaseType(ref reader, typeToConvert, typeInfo, out caseType))
 070                {
 071                    value = default;
 072                    return false;
 73                }
 74
 075                caseTypeInfo = options.GetTypeInfoInternal(caseType);
 076                state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo;
 077                state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.Effective
 078            }
 79
 080            JsonConverter caseConverter = caseTypeInfo.Converter;
 081            if (!caseConverter.TryReadAsObject(ref reader, caseType, options, ref state, out object? caseValue))
 082            {
 083                value = default;
 084                return false;
 85            }
 86
 087            value = constructor(caseType, caseValue);
 088            return true;
 089        }
 90
 91        private static bool TryResolveCaseType(ref Utf8JsonReader reader, Type typeToConvert, JsonTypeInfo<TUnion> typeI
 092        {
 093            caseType = null!;
 094            JsonTypeClassifier? classifier = typeInfo.TypeClassifier;
 095            if (classifier is not null)
 096            {
 97                // Ensure the entire value has been pre-buffered.
 098                Utf8JsonReader readerCopy = reader;
 099                if (!readerCopy.TrySkipPartial())
 0100                {
 0101                    return false;
 102                }
 103
 104                // Pass a copy of the reader to the classifier.
 0105                readerCopy = reader;
 0106                caseType = classifier(ref readerCopy)!;
 107
 0108                if (caseType is null)
 0109                {
 0110                    ThrowHelper.ThrowJsonException_UnionTypeClassifierReturnedNull(typeToConvert, reader.TokenType);
 111                }
 0112            }
 113            else
 0114            {
 115                // Default path: JSON value shape matching. Diagnostics recorded at configure time
 116                // surface here so that ambiguous / custom-converter cases fail with a precise
 117                // message ONLY when a deserialization is actually attempted (config never throws).
 0118                JsonTokenType tokenType = reader.TokenType;
 0119                JsonValueType valueType = GetJsonValueType(tokenType);
 120
 0121                if ((typeInfo.UnionAmbiguousValueTypes & valueType) != 0)
 0122                {
 0123                    ThrowHelper.ThrowJsonException_UnionAmbiguousJsonValueType(typeToConvert, valueType);
 124                }
 125
 0126                Type? resolvedCaseType = null;
 0127                typeInfo.UnionValueTypeMap?.TryGetValue(valueType, out resolvedCaseType);
 0128                caseType = resolvedCaseType!;
 0129            }
 130
 0131            if (caseType is null)
 0132            {
 0133                ThrowHelper.ThrowJsonException_UnionJsonTokenTypeNotSupported(typeToConvert, reader.TokenType);
 134            }
 135
 0136            return true;
 0137        }
 138
 139        private static JsonValueType GetJsonValueType(JsonTokenType tokenType) =>
 0140            tokenType switch
 0141            {
 0142                JsonTokenType.StartObject => JsonValueType.Object,
 0143                JsonTokenType.StartArray => JsonValueType.Array,
 0144                JsonTokenType.String => JsonValueType.String,
 0145                JsonTokenType.Number => JsonValueType.Number,
 0146                JsonTokenType.True or JsonTokenType.False => JsonValueType.Boolean,
 0147                JsonTokenType.Null => JsonValueType.Null,
 0148                _ => JsonValueType.None,
 0149            };
 150
 151        internal override bool OnTryWrite(Utf8JsonWriter writer, TUnion value, JsonSerializerOptions options, ref WriteS
 0152        {
 0153            if (value is null)
 0154            {
 0155                writer.WriteNullValue();
 0156                return true;
 157            }
 158
 159            // Union converters delegate inline to the case converter without emitting
 160            // structural JSON tokens (StartObject/StartArray), so the writer's depth
 161            // does not increase across union-to-union recursion. Guard against cyclic
 162            // union references using the WriteStack frame depth instead.
 0163            if (state.CurrentDepth >= options.EffectiveMaxDepth)
 0164            {
 0165                ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth);
 166            }
 167
 0168            JsonTypeInfo<TUnion> typeInfo = (JsonTypeInfo<TUnion>)state.Current.JsonTypeInfo;
 169
 0170            Func<TUnion, (Type?, object?)>? deconstructor = typeInfo.UnionDeconstructor;
 0171            if (deconstructor is null)
 0172            {
 0173                ThrowHelper.ThrowJsonException_UnionCannotReadValue(typeof(TUnion));
 174            }
 175
 0176            (Type? caseType, object? caseValue) = deconstructor(value);
 177
 0178            if (caseType is null)
 0179            {
 180                // A null case type signals the canonical "null union" state. Both members
 181                // of the deconstructor tuple convey that signal: caseType doubles as the
 182                // discriminator, and a non-null caseValue here is ignored.
 0183                writer.WriteNullValue();
 0184                return true;
 185            }
 186
 0187            JsonTypeInfo caseTypeInfo = options.GetTypeInfoInternal(caseType);
 0188            state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo;
 0189            state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.EffectiveNumb
 0190            return caseTypeInfo.Converter.TryWriteAsObject(writer, caseValue, options, ref state);
 0191        }
 192    }
 193}
 194