From 3ca037548fb4e2841769d16b846a7caea1a45dd1 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Sat, 15 Aug 2026 21:35:03 +0200 Subject: [PATCH 1/5] Clarify two STJ behaviors in the README The generator runs in metadata mode, not fast-path mode: System.Text.Json only fast-paths types without a custom converter. And unlike native [JsonDerivedType] polymorphism (which needs AllowOutOfOrderMetadataProperties for a mid-object discriminator), the converter reads the discriminator from anywhere. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e685c4..cb776df 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,7 @@ public interface IExpression { } - Deeply nested graphs need `MaxDepth` about one level higher than with the Newtonsoft/plain serialization: the discriminator write path round-trips through a `JsonDocument`, which consumes one depth level. (A 64-level chain requires `MaxDepth = 66` instead of 65.) - Name-based type resolution stays scoped to the base type's assembly by default. Cross-assembly subtypes require an explicit opt-in: `[KnownSubTypeOtherAssembly("AssemblyName")]` on the base type, a capability the Newtonsoft version does not have. - `JsonNamingPolicy` and `PropertyNameCaseInsensitive` are respected when matching the discriminator property, and `JsonStringEnumConverter` is respected when mapping discriminator values. Note that `JsonStringEnumConverter` (.NET 8) does **not** honor `[EnumMember(Value = ...)]` — use enum names or `[JsonStringEnumMemberName]` (.NET 9+). +- The discriminator is read from anywhere in the object. Native `[JsonDerivedType]` polymorphism requires its `$type` property first, unless you opt into `JsonSerializerOptions.AllowOutOfOrderMetadataProperties`. - Dotted or nested discriminator property paths (e.g. `"nested.property"`) are supported. - **Fallback paths**: serializing the base type itself (rather than a subtype) and deserializing an unknown discriminator back to the base use a reflection-based writer/reader, because the base type's contract is owned by the converter (`System.Text.Json` exposes no property metadata for converter-owned types). `[JsonPropertyName]`, `[JsonIgnore]` (including `JsonIgnoreCondition`), the naming policy and `DefaultIgnoreCondition` are honored; per-property `[JsonConverter]`, `[JsonInclude]` fields, `required` members and parameterized constructors are not supported on these two paths. - **Performance**: writing an object with a discriminator serializes it once, then re-parses the JSON (`JsonDocument`) to inject the discriminator property, so payloads spend roughly 2-3x their size in temporary memory on the write path. This is the cost of the converter architecture and of the `MaxDepth + 1` note above. @@ -406,7 +407,7 @@ Only types assignable from the polymorphic base type can be resolved, but any su 1. **Converter (`Build()`)** — the full-featured runtime engine and the right default for non-AOT applications. 2. **Resolver (`BuildResolver()`)** — the thin native bridge: simplest and fastest, but limited to the subset the native contract model can express. -3. **Generator (`JsonSubTypes.Text.Json.Aot`)** — a Roslyn source generator emitting compiled converters: the Native AOT answer, with routing compiled instead of reflected. +3. **Generator (`JsonSubTypes.Text.Json.Aot`)** — a Roslyn source generator emitting compiled converters: the Native AOT answer, with routing compiled instead of reflected. Note: this still runs in `System.Text.Json`'s metadata mode, not its fast-path mode — `System.Text.Json` only fast-paths types it has no custom converter for. **The decisive difference is not speed, it is when the hierarchy is known:** From c03911c024d791368bfbf41689f0baf95f35a65c Mon Sep 17 00:00:00 2001 From: manuc66 Date: Sat, 15 Aug 2026 22:17:07 +0200 Subject: [PATCH 2/5] Validate RegisterDynamicSubtype, cache the mapping key type, drop per-level LINQ Code review follow-ups: - RegisterDynamicSubtype now rejects null, abstract, interface or non-assignable types instead of silently breaking the converter's invariants. - Cache the discriminator key type per converter (the generated converter compiles it) instead of scanning the mapping keys on every object, and make it volatile so a concurrent registration is visible. A registration racing a deserialization only affects the cache, never the mapping. - GetTypeResolver scans the converter array directly with an excluded resolver instead of rebuilding a LINQ Where iterator on every multi-level walk step. --- JsonSubTypes.Text.Json/JsonSubtypes.cs | 120 ++++++++++++++++--------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index 5cea367..b3e352c 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -149,6 +149,12 @@ private static readonly ConditionalWeakTable? _runtimeTypeToDiscriminator; private readonly Assembly[] _additionalAssemblies; + // Cached discriminator key type, so GetTypeFromMapping does not re-scan the mapping keys on + // every object. Invalidate with RegisterDynamicSubtype when the mapping mutates. volatile so a + // concurrent registration is visible to readers; a registration racing a deserialization + // ("last writer wins") is an unusual usage and only affects the cache, never the mapping. + private volatile Type? _mappingKeyType; + public JsonSubtypes() { _additionalAssemblies = []; @@ -199,6 +205,15 @@ public override bool CanConvert(Type objectType) /// public void RegisterDynamicSubtype(object discriminator, Type type) { + ArgumentNullException.ThrowIfNull(discriminator); + ArgumentNullException.ThrowIfNull(type); + + if (type.IsAbstract || type.IsInterface || !typeof(T).IsAssignableFrom(type)) + { + throw new ArgumentException( + $"Type {type.FullName} is not a concrete subtype assignable from {typeof(T).FullName}.", nameof(type)); + } + if (_subTypeMapping == null) { throw new InvalidOperationException( @@ -206,6 +221,7 @@ public void RegisterDynamicSubtype(object discriminator, Type type) } _subTypeMapping.Set(discriminator, type); + _mappingKeyType = null; if (_runtimeTypeToDiscriminator != null) { _runtimeTypeToDiscriminator[type] = discriminator; @@ -612,7 +628,7 @@ private Type GetType(JsonDocument jObject, Type parentType, JsonSerializerOption [.. s.Converters.OfType()]); Type targetType = parentType; - IJsonSubtypes? currentTypeResolver = GetTypeResolver(targetType.GetTypeInfo(), converters); + IJsonSubtypes? currentTypeResolver = GetTypeResolver(targetType.GetTypeInfo(), converters, null); if (currentTypeResolver == null) { return targetType; @@ -627,8 +643,7 @@ private Type GetType(JsonDocument jObject, Type parentType, JsonSerializerOption // Single-level resolution is the common case: only allocate the nested // walk (and its cycle-protection set) when the resolved type carries its // own resolver, i.e. for multi-level hierarchies. - IJsonSubtypes? nestedResolver = GetTypeResolver(targetType.GetTypeInfo(), - converters.Where(c => c != currentTypeResolver)); + IJsonSubtypes? nestedResolver = GetTypeResolver(targetType.GetTypeInfo(), converters, currentTypeResolver); if (nestedResolver == null) { return targetType; @@ -646,14 +661,14 @@ private Type GetType(JsonDocument jObject, Type parentType, JsonSerializerOption } lastTypeResolver = currentTypeResolver; - currentTypeResolver = GetTypeResolver(targetType.GetTypeInfo(), - converters.Where(c => c != currentTypeResolver)); + currentTypeResolver = GetTypeResolver(targetType.GetTypeInfo(), converters, currentTypeResolver); } return targetType; } - private IJsonSubtypes? GetTypeResolver(TypeInfo? targetType, IEnumerable jsonConverterCollection) + private IJsonSubtypes? GetTypeResolver(TypeInfo? targetType, IJsonSubtypes[] jsonConverters, + IJsonSubtypes? excluded) { if (targetType == null) { @@ -674,7 +689,17 @@ private Type GetType(JsonDocument jObject, Type parentType, JsonSerializerOption static key => CreateTypeResolver(key.Item2)); } - return jsonConverterCollection.FirstOrDefault(c => c.CanConvert(target)); + // Linear scan without a LINQ Where iterator; the collection is small and this is only + // hit on multi-level hierarchies. + foreach (IJsonSubtypes converter in jsonConverters) + { + if (converter != excluded && converter.CanConvert(target)) + { + return converter; + } + } + + return null; } private static IJsonSubtypes CreateTypeResolver(Type targetType) @@ -860,58 +885,67 @@ private static bool TryGetProperty(JsonElement obj, string name, JsonSerializerO return null; } - private static Type? GetTypeFromMapping(NullableDictionary typeMapping, + private Type? GetTypeFromMapping(NullableDictionary typeMapping, JsonElement discriminatorToken, JsonSerializerOptions jsonSerializerOptions) { if (discriminatorToken.ValueKind == JsonValueKind.Null) { - typeMapping.TryGetValue(null, out Type? targetType); + typeMapping.TryGetValue(null, out Type? nullTarget); - return targetType; + return nullTarget; } - object? key = typeMapping.NotNullKeys().FirstOrDefault(); - if (key != null) + // The discriminator key type is a property of the mapping, not of the token. Cache it + // instead of scanning the keys on every object (the generated converter compiles it). + Type? keyType = _mappingKeyType; + if (keyType == null) { - // Fast path: for the dominant string/int mappings, compare the token directly - // instead of round-tripping through GetRawText() + JsonSerializer.Deserialize. - if (key is string && discriminatorToken.ValueKind == JsonValueKind.String) - { - string? stringValue = discriminatorToken.GetString(); - if (stringValue != null && typeMapping.TryGetValue(stringValue, out Type? stringTarget)) - { - return stringTarget; - } + keyType = typeMapping.NotNullKeys().FirstOrDefault()?.GetType(); + _mappingKeyType = keyType; + } - return null; - } + if (keyType == null) + { + return null; + } - if (key is int && discriminatorToken.TryGetInt32(out int intValue)) + // Fast path: for the dominant string/int mappings, compare the token directly + // instead of round-tripping through GetRawText() + JsonSerializer.Deserialize. + if (keyType == typeof(string) && discriminatorToken.ValueKind == JsonValueKind.String) + { + string? stringValue = discriminatorToken.GetString(); + if (stringValue != null && typeMapping.TryGetValue(stringValue, out Type? stringTarget)) { - if (typeMapping.TryGetValue(intValue, out Type? intTarget)) - { - return intTarget; - } - - return null; + return stringTarget; } - Type targetLookupValueType = key.GetType(); - object? lookupValue; - try - { - lookupValue = JsonSerializer.Deserialize(discriminatorToken.GetRawText(), targetLookupValueType, - jsonSerializerOptions); - } - catch (JsonException) - { - return null; - } + return null; + } - if (typeMapping.TryGetValue(lookupValue, out Type? targetType)) + if (keyType == typeof(int) && discriminatorToken.TryGetInt32(out int intValue)) + { + if (typeMapping.TryGetValue(intValue, out Type? intTarget)) { - return targetType; + return intTarget; } + + return null; + } + + object? lookupValue; + try + { + lookupValue = JsonSerializer.Deserialize(discriminatorToken.GetRawText(), keyType, + jsonSerializerOptions); + } + catch (JsonException) + { + return null; + } + + if (typeMapping.TryGetValue(lookupValue, out Type? targetType)) + { + return targetType; } return null; From c051f3f7721cae12729ba161f877e168bee69d6b Mon Sep 17 00:00:00 2001 From: manuc66 Date: Sat, 15 Aug 2026 23:16:23 +0200 Subject: [PATCH 3/5] Stream the discriminator write with Utf8JsonReader instead of JsonDocument The payload is written into a compact buffer we just produced, so re-reading it token by token with a Utf8JsonReader avoids materializing a JsonDocument DOM. The discriminator is injected first or last and the payload property of the same name is skipped. Values are copied with a small recursive copier; numbers use WriteRawValue to preserve the exact token (decimals, exponents, big ints). Measured (BenchmarkDotNet, net10, DefaultJob): Single_Converter_Serialize 1.15us -> 1.00us, Col_Converter_Serialize 4.23us -> 3.41us. All 200 STJ tests pass. --- JsonSubTypes.Text.Json/JsonSubtypes.cs | 102 ++++++++++++++++++++----- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index b3e352c..fc71534 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -340,36 +340,100 @@ private void WriteObjectWithDiscriminator(Utf8JsonWriter writer, ReadOnlyMemory< string discriminatorJson = JsonSerializer.Serialize(discriminatorValue, serializer); - using JsonDocument document = JsonDocument.Parse(json); - + // Stream the payload with a Utf8JsonReader instead of materializing a JsonDocument: the + // payload was just written by us into a compact buffer, so re-reading it token by token + // avoids the DOM allocations. The discriminator is written first or last and the payload + // property of the same name (if any) is skipped. + Utf8JsonReader reader = new(json.Span); + reader.Read(); // StartObject + + writer.WriteStartObject(); if (_addDiscriminatorFirst) { - writer.WriteStartObject(); writer.WritePropertyName(discriminatorName); writer.WriteRawValue(discriminatorJson, skipInputValidation: true); - foreach (JsonProperty property in document.RootElement.EnumerateObject()) - { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); } - else + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - writer.WriteStartObject(); - foreach (JsonProperty property in document.RootElement.EnumerateObject()) + ReadOnlySpan propertyName = reader.ValueSpan; + bool isDiscriminator = reader.ValueTextEquals(discriminatorName); + reader.Read(); + if (isDiscriminator) { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } + SkipValue(ref reader); + } + else + { + writer.WritePropertyName(propertyName); + CopyValue(ref reader, writer); } + } + + if (!_addDiscriminatorFirst) + { writer.WritePropertyName(discriminatorName); writer.WriteRawValue(discriminatorJson, skipInputValidation: true); - writer.WriteEndObject(); } + writer.WriteEndObject(); + } + + private static void CopyValue(ref Utf8JsonReader reader, Utf8JsonWriter writer) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + writer.WriteStartObject(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + writer.WritePropertyName(reader.ValueSpan); + reader.Read(); + CopyValue(ref reader, writer); + } + writer.WriteEndObject(); + break; + case JsonTokenType.StartArray: + writer.WriteStartArray(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + CopyValue(ref reader, writer); + } + writer.WriteEndArray(); + break; + case JsonTokenType.String: + writer.WriteStringValue(reader.GetString()); + break; + case JsonTokenType.Number: + // Preserve the exact token (decimals, exponents, big ints). Numbers inside an + // indented array stay compact, which is numerically exact. + writer.WriteRawValue(reader.ValueSpan, skipInputValidation: true); + break; + case JsonTokenType.True: + writer.WriteBooleanValue(true); + break; + case JsonTokenType.False: + writer.WriteBooleanValue(false); + break; + case JsonTokenType.Null: + writer.WriteNullValue(); + break; + } + } + + private static void SkipValue(ref Utf8JsonReader reader) + { + int depth = 0; + do + { + if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) + { + depth++; + } + else if (reader.TokenType is JsonTokenType.EndObject or JsonTokenType.EndArray) + { + depth--; + } + } while (depth > 0 && reader.Read()); } private static Action BuildBaseTypeWriter(Type type) From 5e45a5b39699eb7e69c7cb85971a6ba1985f8eb0 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Sat, 15 Aug 2026 23:19:52 +0200 Subject: [PATCH 4/5] Use the declared property type in the base writer and reject mixed discriminator types Code review follow-ups: - BuildBaseTypeWriter serialized each property as object (runtime type); STJ uses the declared property type so a polymorphic converter on that type applies. The reader already used the declared type, so the writer now matches. - RegisterDynamicSubtype rejected abstract/interface types; it now also rejects a discriminator whose type differs from the existing keys, which would make the cached key type inconsistent. --- JsonSubTypes.Text.Json.Tests/ReviewBugTests.cs | 13 +++++++++++++ JsonSubTypes.Text.Json/JsonSubtypes.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/JsonSubTypes.Text.Json.Tests/ReviewBugTests.cs b/JsonSubTypes.Text.Json.Tests/ReviewBugTests.cs index 1dd5302..79ee8b0 100644 --- a/JsonSubTypes.Text.Json.Tests/ReviewBugTests.cs +++ b/JsonSubTypes.Text.Json.Tests/ReviewBugTests.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Json; using System.Text.Json.Serialization; using JsonSubTypes.Text.Json; @@ -286,6 +287,18 @@ public void DynamicSubtypeRegisteredAtRuntime() Assert.IsTrue((dog as SelfDeclaredDog)?.CanBark == true); } + [Test] + public void RegisterDynamicSubtypeRejectsMixedDiscriminatorTypes() + { + var converter = (JsonSubtypes)JsonSubtypesConverterBuilder + .Of("Kind") + .Build(); + converter.RegisterDynamicSubtype("dog", typeof(SelfDeclaredDog)); + + // A discriminator of a different type would make the cached key type inconsistent. + Assert.Throws(() => converter.RegisterDynamicSubtype(1, typeof(SelfDeclaredCat))); + } + [Test] public void SelfDeclaredSubtypeByPropertyPresence() { diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index fc71534..8e34311 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -220,6 +220,15 @@ public void RegisterDynamicSubtype(object discriminator, Type type) "RegisterDynamicSubtype requires a builder-built converter. Build one with JsonSubtypesConverterBuilder.Of(...).Build() first."); } + // All discriminators of one converter must share a type (the mapping is resolved by key + // type). Registering a different key type would make the cached key type inconsistent. + object? existingKey = _subTypeMapping.NotNullKeys().FirstOrDefault(); + if (existingKey != null && !existingKey.GetType().IsInstanceOfType(discriminator)) + { + throw new ArgumentException( + $"Discriminator type {discriminator.GetType().FullName} does not match the existing discriminators of type {existingKey.GetType().FullName}.", nameof(discriminator)); + } + _subTypeMapping.Set(discriminator, type); _mappingKeyType = null; if (_runtimeTypeToDiscriminator != null) @@ -475,7 +484,10 @@ .. type } writer.WritePropertyName(name); - JsonSerializer.Serialize(writer, propertyValue, serializer); + // Use the declared property type, not the runtime type: System.Text.Json + // serializes a property according to its declared contract, and a polymorphic + // converter on that declared type must be applied. + JsonSerializer.Serialize(writer, propertyValue, item.Property.PropertyType, serializer); } writer.WriteEndObject(); }; From 960585f27dbec94d8585e9125144849445d70af0 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Sat, 15 Aug 2026 23:42:08 +0200 Subject: [PATCH 5/5] Modernize JsonSubtypes.cs: private field, pattern matching, nullable fixes - JsonDiscriminatorPropertyName is private (was protected); no internal code or test derives-and-uses it. - Write takes T? and ReadPlainObject returns T (non-nullable), matching their actual contracts. - Replace verbose conditions with pattern matching (is {...}, [..^], ?.ConverterType). - Drop the volatile on _mappingKeyType: it only protected a cache field whose stale-read risk is benign, and RegisterDynamicSubtype documents its setup-time contract instead. --- JsonSubTypes.Text.Json/JsonSubtypes.cs | 53 ++++++++++---------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index 8e34311..f462055 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -4,7 +4,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -139,7 +138,7 @@ private static readonly ConcurrentDictionary OptionsConverterCache = new(); - protected readonly string? JsonDiscriminatorPropertyName; + private readonly string? _jsonDiscriminatorPropertyName; private readonly NullableDictionary? _subTypeMapping; private readonly List? _typesByPropertyPresence; @@ -150,10 +149,8 @@ private static readonly ConditionalWeakTable to without editing the plugin or /// scanning an assembly. The last registration for a discriminator wins, like the builder. /// + /// + /// Call this during setup, before the converter is used for serialization. It mutates the + /// mapping and its caches, which is not safe concurrently with serialization on another + /// thread; the mapping is otherwise read-only once built. + /// public void RegisterDynamicSubtype(object discriminator, Type type) { ArgumentNullException.ThrowIfNull(discriminator); @@ -242,7 +244,7 @@ public void RegisterDynamicSubtype(object discriminator, Type type) return ReadJson(ref reader, objectType, serializer); } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions serializer) + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions serializer) { if (value is null) { @@ -252,7 +254,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions Type runtimeType = value.GetType(); - if (JsonDiscriminatorPropertyName == null) + if (_jsonDiscriminatorPropertyName == null) { WritePlain(writer, value, runtimeType, serializer); return; @@ -341,7 +343,7 @@ private static void ThrowImpossibleToSerialize(Type runtimeType) private void WriteObjectWithDiscriminator(Utf8JsonWriter writer, ReadOnlyMemory json, object? discriminatorValue, JsonSerializerOptions serializer) { - string discriminatorName = JsonDiscriminatorPropertyName!; + string discriminatorName = _jsonDiscriminatorPropertyName!; if (serializer.PropertyNamingPolicy != null) { discriminatorName = serializer.PropertyNamingPolicy.ConvertName(discriminatorName); @@ -504,7 +506,7 @@ private readonly struct BaseTypeWriteProperty private static bool IsIgnoredOnRead(JsonIgnoreAttribute? jsonIgnore) { - return jsonIgnore != null && jsonIgnore.Condition == JsonIgnoreCondition.Always; + return jsonIgnore is { Condition: JsonIgnoreCondition.Always }; } private static bool ShouldIgnore(object? defaultValue, JsonIgnoreAttribute? jsonIgnore, @@ -546,7 +548,7 @@ private static bool IsDefaultValue(object? value, object? defaultValue) private static readonly ConcurrentDictionary> BaseTypeFactoryCache = new(); - private static T? ReadPlainObject(JsonElement jObject, Type targetType, JsonSerializerOptions serializer) + private static T ReadPlainObject(JsonElement jObject, Type targetType, JsonSerializerOptions serializer) { object instance; try @@ -605,7 +607,7 @@ .. type switch (reader.TokenType) { case JsonTokenType.Null: - return default; + return null; case JsonTokenType.StartObject: return ReadObject(ref reader, objectType, serializer); case JsonTokenType.StartArray: @@ -680,20 +682,12 @@ private static IList CreateCompatibleList(Type targetContainerType, Type element return ReadPlainObject(jObject.RootElement, targetType, serializer); } - return (T?)JsonSerializer.Deserialize(jObject.RootElement, targetType, serializer); + return (T?)jObject.RootElement.Deserialize(targetType, serializer); } Type IJsonSubtypes.GetType(JsonDocument jObject, Type parentType, JsonSerializerOptions jsonSerializerOptions) { - Type? resolvedType; - if (JsonDiscriminatorPropertyName == null) - { - resolvedType = GetTypeByPropertyPresence(jObject, parentType, jsonSerializerOptions); - } - else - { - resolvedType = GetTypeFromDiscriminatorValue(jObject, parentType, jsonSerializerOptions); - } + Type? resolvedType = _jsonDiscriminatorPropertyName == null ? GetTypeByPropertyPresence(jObject, parentType, jsonSerializerOptions) : GetTypeFromDiscriminatorValue(jObject, parentType, jsonSerializerOptions); return resolvedType ?? GetFallbackSubType(parentType) ?? parentType; } @@ -755,10 +749,7 @@ private Type GetType(JsonDocument jObject, Type parentType, JsonSerializerOption JsonSubTypeConverterAttribute? jsonConverterAttribute = ConverterAttributeCache.GetOrAdd(target, static type => GetAttribute(type.GetTypeInfo())); - if (jsonConverterAttribute != null && - jsonConverterAttribute.ConverterType != null && - jsonConverterAttribute.ConverterType.IsGenericType && - jsonConverterAttribute.ConverterType.GenericTypeArguments.Length > 0 && + if (jsonConverterAttribute?.ConverterType is { IsGenericType: true, GenericTypeArguments.Length: > 0 } && typeof(T).IsAssignableFrom(jsonConverterAttribute.ConverterType.GenericTypeArguments[0])) { return AttributeResolverCache.GetOrAdd((typeof(T), target), @@ -841,8 +832,8 @@ .. GetAttributes(parentType.GetTypeInfo()) private Type? GetTypeFromDiscriminatorValue(JsonDocument jObject, Type parentType, JsonSerializerOptions jsonSerializerOptions) { - if (JsonDiscriminatorPropertyName == null || - !TryGetValueInJson(jObject.RootElement, JsonDiscriminatorPropertyName, jsonSerializerOptions, + if (_jsonDiscriminatorPropertyName == null || + !TryGetValueInJson(jObject.RootElement, _jsonDiscriminatorPropertyName, jsonSerializerOptions, out JsonElement discriminatorValue)) { return null; @@ -936,9 +927,7 @@ private static bool TryGetProperty(JsonElement obj, string name, JsonSerializerO } string? parentTypeFullName = parentType.FullName; - string? searchLocation = parentTypeFullName == null - ? null - : parentTypeFullName.Substring(0, parentTypeFullName.Length - parentType.Name.Length); + string? searchLocation = parentTypeFullName?[..^parentType.Name.Length]; Assembly[] attributeAssemblies = TypeResolution.GetSearchAssemblies(parentType); IEnumerable assemblies = attributeAssemblies