Skip to content

Runtime plugin pattern: KnownSubTypeOf, RegisterSubtypeAssembly, RegisterDynamicSubtype - #211

Open
manuc66 wants to merge 5 commits into
feature/split/pr4-migration-fixesfrom
feature/split/pr5-plugin-pattern
Open

Runtime plugin pattern: KnownSubTypeOf, RegisterSubtypeAssembly, RegisterDynamicSubtype#211
manuc66 wants to merge 5 commits into
feature/split/pr4-migration-fixesfrom
feature/split/pr5-plugin-pattern

Conversation

@manuc66

@manuc66 manuc66 commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Problem

Runtime-loaded subtypes (plugins, assemblies loaded at runtime, config-driven types) could not self-declare in the STJ engine: the only cross-assembly mechanism was a process-wide assembly registry, and per-subtype runtime registration did not exist.

Fix

  • Replace the global JsonSubTypesTypeResolution.AddAssembly registry with a declarative [KnownSubTypeOtherAssembly("AssemblyName")] attribute on the base type (per-type, no process-wide leak; no compile-time reference to the plugin).
  • Add self-declaring subtypes via KnownSubTypeOf (with discriminator value or by type name) and KnownSubTypeWithPropertyOf (by property presence).
  • Add RegisterSubtypeAssembly(assembly) to scan a loaded assembly for those declarations.
  • Add RegisterDynamicSubtype(discriminator, type) to register a single subtype directly after the converter is built.
  • New JsonSubTypes.Text.Json.Tests.Plugin test project.

Tests

  • New plugin/self-declaring tests across STJ suites; full STJ suite passes (net8.0 and net10.0).

Honest note(s)

  • The generator cannot see runtime-loaded hierarchies; the converter is the only engine for them (documented in the README plugin section, added in a later PR).
  • RegisterDynamicSubtype mutates the mapping and is intended for setup time, not concurrent use with serialization.

…ypeResolution attribute

JsonSubTypesTypeResolution.AddAssembly was a process-wide mutable registry that
leaked state across serialization profiles. Replace it with
[JsonSubTypesTypeResolution("AssemblyName")] on the base type, cached per type.

The attribute takes an assembly name rather than a Type: a Type would force the
base to reference the plugin assembly, which together with the plugin's reference
to the base creates a project cycle. Naming the assembly keeps the base free of a
compile-time reference to the plugin.

The cross-assembly tests now use SharedAnimal (base) with the plugin assembly
declared on it and PluginDog (in the plugin project) resolved by name, plus a
negative test on a base without the attribute.
The attribute's previous name was inherited from the global AddAssembly registry
it replaced and said nothing about its contract. KnownSubTypeOtherAssembly is a
noun consistent with the KnownSubType family, and Other carries the extension
beyond the base type's own assembly. The file follows the class name.
…SubtypeAssembly

Support the inverse philosophy of the base-declares approach: the subtype declares
itself as a child of the base through [KnownSubTypeOf(typeof(Base), "value")],
and the host registers the plugin assembly at runtime through
RegisterSubtypeAssembly. This covers the real plugin scenario the attribute
cannot: assemblies loaded at runtime whose names are unknown at compile time.

The scan mirrors the runtime cross-assembly approach: register the assembly, scan
its types for self-declared subtypes, and map those that carry a discriminator
value; types without a value resolve by name in the registered assembly.
ReflectionTypeLoadException is handled so an assembly with unloadable optional
types is not fatal. Both philosophies coexist: the base can still name other
assemblies via [KnownSubTypeOtherAssembly].
The generator already exposed RegisterDynamicSubtype, but the runtime converter
did not: README claimed parity that did not exist. Add the same runtime hook to
JsonSubtypes<T>, mapping a discriminator to a subtype after the converter is
built, with last-registration-wins like the generator. This covers the simple
plugin need (register a subtype at runtime) without editing the plugin or
scanning an assembly, complementing RegisterSubtypeAssembly.

NullableDictionary gains a Set method for the overwrite semantics.
…ropertyOf

Mirror the KnownSubTypeOf work for property-presence discrimination. The subtype
declares itself through [KnownSubTypeWithPropertyOf(typeof(Base), "Property")]
and JsonSubtypesWithPropertyConverterBuilder.RegisterSubtypeAssembly scans the
registered assembly for those declarations, mapping the property presence. Same
ReflectionTypeLoadException handling as the value-based scan.
Sign up for free to 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