| File: System\Text\Json\Serialization\JsonSerializer.Read.HandleMetadata.cs | Web Access |
| Project: 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.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json { public static partial class JsonSerializer { internal const string IdPropertyName = "$id"; internal const string RefPropertyName = "$ref"; internal const string TypePropertyName = "$type"; internal const string ValuesPropertyName = "$values"; private static readonly byte[] s_idPropertyName = "$id"u8.ToArray(); private static readonly byte[] s_refPropertyName = "$ref"u8.ToArray(); private static readonly byte[] s_typePropertyName = "$type"u8.ToArray(); private static readonly byte[] s_valuesPropertyName = "$values"u8.ToArray(); internal static bool TryReadMetadata(JsonConverter converter, JsonTypeInfo jsonTypeInfo, ref Utf8JsonReader reader, scoped ref ReadStack state) { Debug.Assert(state.Current.ObjectState == StackFrameObjectState.StartToken); Debug.Assert(state.Current.CanContainMetadata); // Custom classifier branch: runs BEFORE normal metadata scanning. // When a TypeClassifier is configured, it replaces discriminator-based type resolution. PolymorphicTypeResolver? polymorphicResolver = jsonTypeInfo.PolymorphicTypeResolver; if (polymorphicResolver is not null && jsonTypeInfo.TypeClassifier is { } typeClassifier) { // Ensure the entire object is buffered (for streaming scenarios). if (!reader.IsFinalBlock) { Utf8JsonReader bufferCheck = reader; if (!bufferCheck.TrySkipPartial()) { return false; } } // If reference handling is configured and the payload is a $ref-only object, // defer to the normal metadata loop so the reference resolver can return the // previously-deserialized instance instead of asking the classifier to identify // a type from a metadata-only payload that does not describe one. bool isRefOnlyPayload = false; if (state.ReferenceResolver is not null) { Utf8JsonReader peek = reader; if (peek.Read() && peek.TokenType == JsonTokenType.PropertyName && peek.GetUnescapedSpan().SequenceEqual(s_refPropertyName)) { isRefOnlyPayload = true; } } if (!isRefOnlyPayload) { // Classify using a struct copy; the original reader stays at the object start. Utf8JsonReader classifierCopy = reader; Type? resolvedType = typeClassifier(ref classifierCopy); if (resolvedType is null) { ThrowHelper.ThrowJsonException(SR.PolymorphicTypeClassifierReturnedNull); } state.PolymorphicResolvedType = resolvedType; } } Utf8JsonReader checkpoint; bool allowOutOfOrderMetadata = jsonTypeInfo.Options.AllowOutOfOrderMetadataProperties; bool isReadingAheadOfNonMetadataProperties = false; if (allowOutOfOrderMetadata && !reader.IsFinalBlock) { // When reading ahead for metadata properties, we need to make sure // that the entire object has been buffered so that we can roll back // the reader state when the read-ahead has been completed. checkpoint = reader; if (!checkpoint.TrySkipPartial()) { return false; } } else { // Checkpointing not used in this mode. checkpoint = default; } while (true) { if (state.Current.PropertyState == StackFramePropertyState.None) { // Read the property name. if (!reader.Read()) { Debug.Assert(!allowOutOfOrderMetadata, "Object must have already been buffered in this mode."); return false; } state.Current.PropertyState = StackFramePropertyState.ReadName; } if (state.Current.PropertyState < StackFramePropertyState.Name) { if (reader.TokenType == JsonTokenType.EndObject) { // Read the entire object while parsing for metadata. goto Done; } // We just read a property. The only valid next tokens are EndObject and PropertyName. Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Ref) != 0) { // No properties whatsoever should follow a $ref property. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetUnescapedSpan(), ref state); } ReadOnlySpan<byte> propertyName = reader.GetUnescapedSpan(); switch (state.Current.LatestMetadataPropertyName = GetMetadataPropertyName(propertyName, jsonTypeInfo.PolymorphicTypeResolver)) { case MetadataPropertyName.Id: state.Current.JsonPropertyName = s_idPropertyName; if (state.ReferenceResolver is null) { // Found an $id property in a type that doesn't support reference preservation ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); } if ((state.Current.MetadataPropertyNames & (MetadataPropertyName.Id | MetadataPropertyName.Ref)) != 0) { // No $id or $ref properties should precede $id properties. ThrowHelper.ThrowJsonException_MetadataIdCannotBeCombinedWithRef(propertyName, ref state); } if (!converter.CanHaveMetadata) { // Should not be permitted unless the converter is capable of handling metadata. ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.Type!); } break; case MetadataPropertyName.Ref: state.Current.JsonPropertyName = s_refPropertyName; if (state.ReferenceResolver is null) { // Found a $ref property in a type that doesn't support reference preservation ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); } if (converter.IsValueType) { // Should not be permitted if the converter is a struct. ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.Type!); } if (state.Current.MetadataPropertyNames != 0 || isReadingAheadOfNonMetadataProperties) { // No properties should precede a $ref property. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetUnescapedSpan(), ref state); } break; case MetadataPropertyName.Type: state.Current.JsonPropertyName = jsonTypeInfo.PolymorphicTypeResolver?.CustomTypeDiscriminatorPropertyNameUtf8 ?? s_typePropertyName; if (jsonTypeInfo.PolymorphicTypeResolver is null) { // Found a $type property in a type that doesn't support polymorphism ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); } if (state.PolymorphicTypeDiscriminator is not null) { // Found a duplicate $type property. ThrowHelper.ThrowJsonException_DuplicateMetadataProperty(state.Current.JsonPropertyName); } break; case MetadataPropertyName.Values: state.Current.JsonPropertyName = s_valuesPropertyName; if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Values) != 0) { // Found a duplicate $values property. ThrowHelper.ThrowJsonException_DuplicateMetadataProperty(s_valuesPropertyName); } if (isReadingAheadOfNonMetadataProperties) { // Cannot combine a $values property with other non-metadata properties. ThrowHelper.ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref state, jsonTypeInfo.Type, reader); } break; default: Debug.Assert(state.Current.LatestMetadataPropertyName == MetadataPropertyName.None); // Encountered a non-metadata property if (allowOutOfOrderMetadata) { if (!isReadingAheadOfNonMetadataProperties) { // This is the first non-metadata property, checkpoint the current reader state. isReadingAheadOfNonMetadataProperties = true; checkpoint = reader; } if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Values) != 0) { // Cannot combine a $values property with other non-metadata properties. ThrowHelper.ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref state, jsonTypeInfo.Type, reader); } break; } else { // Exit immediately if no read-ahead is enabled. goto Done; } } state.Current.PropertyState = StackFramePropertyState.Name; } if (state.Current.PropertyState < StackFramePropertyState.ReadValue) { // Read the property value. if (!reader.Read()) { Debug.Assert(!allowOutOfOrderMetadata, "Object must have already been buffered in this mode."); return false; } state.Current.PropertyState = StackFramePropertyState.ReadValue; } Debug.Assert(state.Current.PropertyState == StackFramePropertyState.ReadValue); switch (state.Current.LatestMetadataPropertyName) { case MetadataPropertyName.Id: if (reader.TokenType != JsonTokenType.String) { ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); } if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } state.ReferenceId = reader.GetString(); break; case MetadataPropertyName.Ref: if (reader.TokenType != JsonTokenType.String) { ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); } if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } state.ReferenceId = reader.GetString(); break; case MetadataPropertyName.Type: Debug.Assert(state.PolymorphicTypeDiscriminator is null); switch (reader.TokenType) { case JsonTokenType.String: state.PolymorphicTypeDiscriminator = reader.GetString(); break; case JsonTokenType.Number: state.PolymorphicTypeDiscriminator = reader.GetInt32(); break; default: ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); break; } break; case MetadataPropertyName.Values: if (reader.TokenType != JsonTokenType.StartArray) { ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType); } if (allowOutOfOrderMetadata) { // The $values property contains the collection payload, // checkpoint the current reader and continue the read-ahead. Debug.Assert(!isReadingAheadOfNonMetadataProperties, "must have already verified if non-metadata properties precede $values"); isReadingAheadOfNonMetadataProperties = true; checkpoint = reader; reader.SkipWithVerify(); } else { state.Current.MetadataPropertyNames |= MetadataPropertyName.Values; state.Current.PropertyState = StackFramePropertyState.None; goto Done; // "$values" property contains the nested payload, exit the metadata reader now. } break; default: Debug.Assert(state.Current.LatestMetadataPropertyName == MetadataPropertyName.None); Debug.Assert(allowOutOfOrderMetadata, "should only be reached if reading ahead is required."); reader.SkipWithVerify(); break; } state.Current.MetadataPropertyNames |= state.Current.LatestMetadataPropertyName; state.Current.PropertyState = StackFramePropertyState.None; state.Current.JsonPropertyName = null; } Done: if (state.Current.MetadataPropertyNames is MetadataPropertyName.Values) { // Cannot have a $values property unless there are other metadata properties. ThrowHelper.ThrowJsonException_MetadataStandaloneValuesProperty(ref state, s_valuesPropertyName); } if (isReadingAheadOfNonMetadataProperties) { // Roll back the reader state to the first non-metadata property. reader = checkpoint; } return true; } internal static bool IsMetadataPropertyName(ReadOnlySpan<byte> propertyName, PolymorphicTypeResolver? resolver) { return GetMetadataPropertyName(propertyName, resolver) != MetadataPropertyName.None; } internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan<byte> propertyName, PolymorphicTypeResolver? resolver) { if (propertyName.Length > 0 && propertyName[0] == '$') { switch (propertyName.Length) { case 3 when propertyName.SequenceEqual("$id"u8): return MetadataPropertyName.Id; case 4 when propertyName.SequenceEqual("$ref"u8): return MetadataPropertyName.Ref; case 5 when resolver?.CustomTypeDiscriminatorPropertyNameUtf8 is null && propertyName.SequenceEqual("$type"u8): return MetadataPropertyName.Type; case 7 when propertyName.SequenceEqual("$values"u8): return MetadataPropertyName.Values; } } if (resolver?.CustomTypeDiscriminatorPropertyNameUtf8 is byte[] customTypeDiscriminator && propertyName.SequenceEqual(customTypeDiscriminator)) { return MetadataPropertyName.Type; } return MetadataPropertyName.None; } internal static bool TryHandleReferenceFromJsonElement( ref Utf8JsonReader reader, scoped ref ReadStack state, JsonElement element, [NotNullWhen(true)] out object? referenceValue) { bool refMetadataFound = false; referenceValue = default; if (element.ValueKind == JsonValueKind.Object) { int propertyCount = 0; foreach (JsonProperty property in element.EnumerateObject()) { propertyCount++; if (refMetadataFound) { // There are more properties in an object with $ref. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } else if (property.EscapedNameEquals(s_idPropertyName)) { if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } if (property.Value.ValueKind != JsonValueKind.String) { ThrowHelper.ThrowJsonException_MetadataValueWasNotString(property.Value.ValueKind); } object boxedElement = element; state.ReferenceResolver.AddReference(property.Value.GetString()!, boxedElement); referenceValue = boxedElement; return true; } else if (property.EscapedNameEquals(s_refPropertyName)) { if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } if (propertyCount > 1) { // $ref was found but there were other properties before. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } if (property.Value.ValueKind != JsonValueKind.String) { ThrowHelper.ThrowJsonException_MetadataValueWasNotString(property.Value.ValueKind); } referenceValue = state.ReferenceResolver.ResolveReference(property.Value.GetString()!); refMetadataFound = true; } } } return refMetadataFound; } internal static bool TryHandleReferenceFromJsonNode( ref Utf8JsonReader reader, scoped ref ReadStack state, JsonNode? jsonNode, [NotNullWhen(true)] out object? referenceValue) { bool refMetadataFound = false; referenceValue = default; if (jsonNode is JsonObject jsonObject) { int propertyCount = 0; foreach (KeyValuePair<string, JsonNode?> property in jsonObject) { propertyCount++; if (refMetadataFound) { // There are more properties in an object with $ref. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } else if (property.Key == "$id") { if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } string referenceId = ReadAsStringMetadataValue(property.Value); state.ReferenceResolver.AddReference(referenceId, jsonNode); referenceValue = jsonNode; return true; } else if (property.Key == "$ref") { if (state.ReferenceId is not null) { ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state); } if (propertyCount > 1) { // $ref was found but there were other properties before. ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } string referenceId = ReadAsStringMetadataValue(property.Value); referenceValue = state.ReferenceResolver.ResolveReference(referenceId); refMetadataFound = true; } static string ReadAsStringMetadataValue(JsonNode? jsonNode) { if (jsonNode is JsonValue jsonValue && jsonValue.TryGetValue(out string? value) && value is not null) { return value; } JsonValueKind metadataValueKind = jsonNode?.GetValueKind() ?? JsonValueKind.Null; Debug.Assert(metadataValueKind != JsonValueKind.Undefined); ThrowHelper.ThrowJsonException_MetadataValueWasNotString(metadataValueKind); return null!; } } } return refMetadataFound; } internal static void ValidateMetadataForObjectConverter(ref ReadStack state) { if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Values) != 0) { // Object converters do not support $values metadata. ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(s_valuesPropertyName, ref state); } } internal static void ValidateMetadataForArrayConverter(JsonConverter converter, ref Utf8JsonReader reader, scoped ref ReadStack state) { switch (reader.TokenType) { case JsonTokenType.StartArray: Debug.Assert(state.Current.MetadataPropertyNames is MetadataPropertyName.None || state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values)); break; case JsonTokenType.EndObject: if (state.Current.MetadataPropertyNames != MetadataPropertyName.Ref) { // Read the entire JSON object while parsing for metadata: for collection converters this is only legal for $ref nodes. ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.Type!); } break; default: Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); // Do not tolerate non-metadata properties in collection converters. ThrowHelper.ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref state, converter.Type!, reader); break; } } internal static T ResolveReferenceId<T>(ref ReadStack state) { Debug.Assert(!typeof(T).IsValueType); Debug.Assert(state.ReferenceId is not null); string referenceId = state.ReferenceId; object value = state.ReferenceResolver.ResolveReference(referenceId); state.ReferenceId = null; try { return (T)value; } catch (InvalidCastException) { ThrowHelper.ThrowInvalidOperationException_MetadataReferenceOfTypeCannotBeAssignedToType( referenceId, value.GetType(), typeof(T)); return default!; } } } }