Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Newtonsoft converter performance: fast-path resolution and JValue writes - #209

Merged
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf
Aug 16, 2026
Merged

Newtonsoft converter performance: fast-path resolution and JValue writes#209
manuc66 merged 9 commits into
masterfrom
feature/split/pr3-newtonsoft-perf

Conversation

@manuc66

Copy link
Copy Markdown
Owner

Problem

The Newtonsoft backend lagged the STJ converter on the same fast-path treatment: it still walked the multi-level hierarchy and resolved discriminators via ToObject reflection on every call.

Fix

  • Fast-path single-level type resolution and direct string/int mapping lookup.
  • Write string/int discriminators as a plain JValue, honoring custom converters.
  • Cache the attribute-derived subtype mapping and property-presence list per type.
  • Test two resolver profiles on the same base type do not interfere.
  • Add a base-as-leaf benchmark and validate every benchmark scenario round-trips before measuring.

Tests

  • New cross-backend resolver-isolation test; Newtonsoft and STJ suites pass (net8.0 and net10.0).

Honest note(s)

  • Newtonsoft numbers in PERFORMANCE.md were refreshed after the fast-path work, but the STJ converter column in the same tables is from the pre-streaming state; the follow-up performance refresh PR corrects it.

…lookup
Mirror the System.Text.Json converter optimizations in the Newtonsoft backend.
GetType now resolves the first level with the current converter and only enters
the converter-scan / list / cycle-protection walk for multi-level hierarchies,
and GetTypeFromMapping converts string/int discriminators directly from the
JToken instead of round-tripping through Newtonsoft's full ToObject reflection.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Deserialize 2.54us ->
2.33us, Collection_Deserialize 10.21us -> 7.72us. All 147 Newtonsoft tests pass.
…nverters
JToken.FromObject round-trips the discriminator through full reflection on every
write. For the dominant string/int discriminators build the JValue directly
instead, but only when no converter on the serializer handles that type: a
custom int/string converter must not be silently bypassed, so it keeps the
serializer-aware path.
Adds tests on both backends covering the four branches: string/int without a
converter (fast path) and with a custom converter (serializer-aware path). The
System.Text.Json side always serializes through JsonSerializer, so its tests pin
that custom converters are honored.
Measured (BenchmarkDotNet, net10, DefaultJob): Single_Serialize 1.54us -> 1.31us,
Collection_Serialize 5.51us -> 4.86us.
…t per type
The base JsonSubtypes converter (attribute path) rebuilt the NullableDictionary
from [KnownSubType] and the List from [KnownSubTypeWithProperty] on every
deserialized object. The attributes themselves were cached; the derived mapping
was not. Cache both per type, following the existing NET35/ConcurrentDictionary
pattern of the attribute cache. The builder path is unaffected: it overrides
GetSubTypeMapping to return its own mapping.
Measured (attribute path, 100k objects): 4961 -> 4735 B/op.
The shared caches (attribute-derived mappings keyed by type in Newtonsoft, the
converter list keyed by JsonSerializerOptions in STJ) must not conflate two
converters registered for the same base type. Add tests on both backends that
alternate between two profiles with different discriminator names and mappings,
proving each resolves its own shape.
The Newtonsoft comparison table still showed the pre-optimization numbers. Update
it to the measured values and document the optimizations applied, plus why the
remaining cost is kept (the JObject/JTokenReader double parse is structural).
Serializing/deserializing the polymorphic base type itself exercises the
converter's reflection-built base writer/reader fallback path, which was
documented but never measured.
Each benchmark constructor now verifies that its scenario serializes to the
expected discriminator and deserializes to the expected subtype, failing fast
instead of silently measuring a broken path.
The string/int fast path in GetLookupValue must not bypass a converter
registered on the serializer: like the discriminator write path, keep the
serializer-aware ToObject when one applies, so the lookup matches the
pre-optimization behavior. The converter scan uses an index-based loop,
so the fast path stays allocation-free (measured allocations match the
documented Newtonsoft numbers).
Adds read-path tests with transforming int/string converters pinning that
behavior on the Newtonsoft suite.
Same single-object and collection scenarios as NewtonsoftBenchmarks, so the
discriminator converter can be compared against Newtonsoft's built-in type-name
handling on the same payload shape. Auto writes $type only on members declared
abstract/interface/object (a root declared as the base type never gets $type),
so the single scenario goes through a holder and the base type is abstract.
@manuc66
manuc66 changed the base branch from feature/split/pr2-stj-converter-perf to masterAugust 16, 2026 19:39
@manuc66
manuc66 merged commit 4edb89a into masterAug 16, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@manuc66