| | | 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 | | |
| | | 4 | | using System.Text.Json.Serialization.Metadata; |
| | | 5 | | |
| | | 6 | | namespace 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 | | { |
| | 0 | 15 | | public JsonUnionConverter() |
| | 0 | 16 | | { |
| | | 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. |
| | 0 | 21 | | SupportsMultipleTokenTypes = true; |
| | 0 | 22 | | } |
| | | 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). |
| | 0 | 27 | | public override bool HandleNull => true; |
| | | 28 | | |
| | 0 | 29 | | private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Union; |
| | | 30 | | |
| | | 31 | | internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, s |
| | 0 | 32 | | { |
| | 0 | 33 | | JsonTypeInfo<TUnion> typeInfo = (JsonTypeInfo<TUnion>)state.Current.JsonTypeInfo; |
| | | 34 | | |
| | 0 | 35 | | Func<Type, object?, TUnion>? constructor = typeInfo.UnionConstructor; |
| | 0 | 36 | | if (constructor is null) |
| | 0 | 37 | | { |
| | 0 | 38 | | ThrowHelper.ThrowJsonException_UnionCannotCreateValue(typeToConvert); |
| | | 39 | | } |
| | | 40 | | |
| | 0 | 41 | | if (reader.TokenType is JsonTokenType.Null) |
| | 0 | 42 | | { |
| | 0 | 43 | | Type? nullableCaseType = typeInfo.UnionNullableCaseType; |
| | 0 | 44 | | if (nullableCaseType is null) |
| | 0 | 45 | | { |
| | | 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. |
| | 0 | 51 | | value = default; |
| | 0 | 52 | | return true; |
| | | 53 | | } |
| | | 54 | | |
| | 0 | 55 | | value = constructor(nullableCaseType, null); |
| | 0 | 56 | | return true; |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | JsonTypeInfo caseTypeInfo; |
| | | 60 | | Type caseType; |
| | | 61 | | |
| | 0 | 62 | | if (state.IsContinuation) |
| | 0 | 63 | | { |
| | 0 | 64 | | caseTypeInfo = state.Current.JsonPropertyInfo!.JsonTypeInfo; |
| | 0 | 65 | | caseType = caseTypeInfo.Type; |
| | 0 | 66 | | } |
| | | 67 | | else |
| | 0 | 68 | | { |
| | 0 | 69 | | if (!TryResolveCaseType(ref reader, typeToConvert, typeInfo, out caseType)) |
| | 0 | 70 | | { |
| | 0 | 71 | | value = default; |
| | 0 | 72 | | return false; |
| | | 73 | | } |
| | | 74 | | |
| | 0 | 75 | | caseTypeInfo = options.GetTypeInfoInternal(caseType); |
| | 0 | 76 | | state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo; |
| | 0 | 77 | | state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.Effective |
| | 0 | 78 | | } |
| | | 79 | | |
| | 0 | 80 | | JsonConverter caseConverter = caseTypeInfo.Converter; |
| | 0 | 81 | | if (!caseConverter.TryReadAsObject(ref reader, caseType, options, ref state, out object? caseValue)) |
| | 0 | 82 | | { |
| | 0 | 83 | | value = default; |
| | 0 | 84 | | return false; |
| | | 85 | | } |
| | | 86 | | |
| | 0 | 87 | | value = constructor(caseType, caseValue); |
| | 0 | 88 | | return true; |
| | 0 | 89 | | } |
| | | 90 | | |
| | | 91 | | private static bool TryResolveCaseType(ref Utf8JsonReader reader, Type typeToConvert, JsonTypeInfo<TUnion> typeI |
| | 0 | 92 | | { |
| | 0 | 93 | | caseType = null!; |
| | 0 | 94 | | JsonTypeClassifier? classifier = typeInfo.TypeClassifier; |
| | 0 | 95 | | if (classifier is not null) |
| | 0 | 96 | | { |
| | | 97 | | // Ensure the entire value has been pre-buffered. |
| | 0 | 98 | | Utf8JsonReader readerCopy = reader; |
| | 0 | 99 | | if (!readerCopy.TrySkipPartial()) |
| | 0 | 100 | | { |
| | 0 | 101 | | return false; |
| | | 102 | | } |
| | | 103 | | |
| | | 104 | | // Pass a copy of the reader to the classifier. |
| | 0 | 105 | | readerCopy = reader; |
| | 0 | 106 | | caseType = classifier(ref readerCopy)!; |
| | | 107 | | |
| | 0 | 108 | | if (caseType is null) |
| | 0 | 109 | | { |
| | 0 | 110 | | ThrowHelper.ThrowJsonException_UnionTypeClassifierReturnedNull(typeToConvert, reader.TokenType); |
| | | 111 | | } |
| | 0 | 112 | | } |
| | | 113 | | else |
| | 0 | 114 | | { |
| | | 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). |
| | 0 | 118 | | JsonTokenType tokenType = reader.TokenType; |
| | 0 | 119 | | JsonValueType valueType = GetJsonValueType(tokenType); |
| | | 120 | | |
| | 0 | 121 | | if ((typeInfo.UnionAmbiguousValueTypes & valueType) != 0) |
| | 0 | 122 | | { |
| | 0 | 123 | | ThrowHelper.ThrowJsonException_UnionAmbiguousJsonValueType(typeToConvert, valueType); |
| | | 124 | | } |
| | | 125 | | |
| | 0 | 126 | | Type? resolvedCaseType = null; |
| | 0 | 127 | | typeInfo.UnionValueTypeMap?.TryGetValue(valueType, out resolvedCaseType); |
| | 0 | 128 | | caseType = resolvedCaseType!; |
| | 0 | 129 | | } |
| | | 130 | | |
| | 0 | 131 | | if (caseType is null) |
| | 0 | 132 | | { |
| | 0 | 133 | | ThrowHelper.ThrowJsonException_UnionJsonTokenTypeNotSupported(typeToConvert, reader.TokenType); |
| | | 134 | | } |
| | | 135 | | |
| | 0 | 136 | | return true; |
| | 0 | 137 | | } |
| | | 138 | | |
| | | 139 | | private static JsonValueType GetJsonValueType(JsonTokenType tokenType) => |
| | 0 | 140 | | tokenType switch |
| | 0 | 141 | | { |
| | 0 | 142 | | JsonTokenType.StartObject => JsonValueType.Object, |
| | 0 | 143 | | JsonTokenType.StartArray => JsonValueType.Array, |
| | 0 | 144 | | JsonTokenType.String => JsonValueType.String, |
| | 0 | 145 | | JsonTokenType.Number => JsonValueType.Number, |
| | 0 | 146 | | JsonTokenType.True or JsonTokenType.False => JsonValueType.Boolean, |
| | 0 | 147 | | JsonTokenType.Null => JsonValueType.Null, |
| | 0 | 148 | | _ => JsonValueType.None, |
| | 0 | 149 | | }; |
| | | 150 | | |
| | | 151 | | internal override bool OnTryWrite(Utf8JsonWriter writer, TUnion value, JsonSerializerOptions options, ref WriteS |
| | 0 | 152 | | { |
| | 0 | 153 | | if (value is null) |
| | 0 | 154 | | { |
| | 0 | 155 | | writer.WriteNullValue(); |
| | 0 | 156 | | 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. |
| | 0 | 163 | | if (state.CurrentDepth >= options.EffectiveMaxDepth) |
| | 0 | 164 | | { |
| | 0 | 165 | | ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth); |
| | | 166 | | } |
| | | 167 | | |
| | 0 | 168 | | JsonTypeInfo<TUnion> typeInfo = (JsonTypeInfo<TUnion>)state.Current.JsonTypeInfo; |
| | | 169 | | |
| | 0 | 170 | | Func<TUnion, (Type?, object?)>? deconstructor = typeInfo.UnionDeconstructor; |
| | 0 | 171 | | if (deconstructor is null) |
| | 0 | 172 | | { |
| | 0 | 173 | | ThrowHelper.ThrowJsonException_UnionCannotReadValue(typeof(TUnion)); |
| | | 174 | | } |
| | | 175 | | |
| | 0 | 176 | | (Type? caseType, object? caseValue) = deconstructor(value); |
| | | 177 | | |
| | 0 | 178 | | if (caseType is null) |
| | 0 | 179 | | { |
| | | 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. |
| | 0 | 183 | | writer.WriteNullValue(); |
| | 0 | 184 | | return true; |
| | | 185 | | } |
| | | 186 | | |
| | 0 | 187 | | JsonTypeInfo caseTypeInfo = options.GetTypeInfoInternal(caseType); |
| | 0 | 188 | | state.Current.JsonPropertyInfo = caseTypeInfo.PropertyInfoForTypeInfo; |
| | 0 | 189 | | state.Current.NumberHandling = typeInfo.NumberHandling ?? caseTypeInfo.PropertyInfoForTypeInfo.EffectiveNumb |
| | 0 | 190 | | return caseTypeInfo.Converter.TryWriteAsObject(writer, caseValue, options, ref state); |
| | 0 | 191 | | } |
| | | 192 | | } |
| | | 193 | | } |
| | | 194 | | |