Skip to content

feat: add runtime derived type registration for cross-project polymorphism#132

Merged
xoofx merged 1 commit into
xoofx:mainfrom
fdcastel:feature/runtime-derived-type-registration
Mar 29, 2026
Merged

feat: add runtime derived type registration for cross-project polymorphism#132
xoofx merged 1 commit into
xoofx:mainfrom
fdcastel:feature/runtime-derived-type-registration

Conversation

@fdcastel
Copy link
Copy Markdown
Contributor

Motivation

In layered architectures (clean architecture, plugin systems, modular monoliths), the base type and its derived types often live in different projects:

Core         →  PullMonitorConfiguration (base class)
Network      →  HttpMonitorConfiguration, PingMonitorConfiguration (derived)
Application  →  References both Core and Network

Currently, the only way to register derived types for polymorphic deserialization is via [YamlDerivedType] attributes placed on the base class. Since Core cannot reference Network (circular dependency), the attribute cannot be placed on the base class. The only project that sees both the base and derived types is Application — but SharpYaml provides no way to register derived types from there.

This is a hard blocker for cross-project polymorphism. Current workaround is writing a full custom YamlConverter<TBase> that manually reads tags and dispatches, duplicating what PolymorphismModel already does internally.

Solution

Add runtime derived type registration via a new DerivedTypeMappings dictionary on YamlPolymorphismOptions:

New API

YamlDerivedType — Runtime equivalent of YamlDerivedTypeAttribute:

public sealed class YamlDerivedType
{
    public YamlDerivedType(Type derivedType);                    // default (no discriminator)
    public YamlDerivedType(Type derivedType, string discriminator);
    public YamlDerivedType(Type derivedType, int discriminator);

    public Type DerivedType { get; }
    public string? Discriminator { get; }
    public string? Tag { get; init; }
}

YamlPolymorphismOptions.DerivedTypeMappings — Per-base-type registry:

public IDictionary<Type, IList<YamlDerivedType>> DerivedTypeMappings { get; }

Usage

var options = new YamlSerializerOptions
{
    PolymorphismOptions = new YamlPolymorphismOptions
    {
        DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
        DerivedTypeMappings =
        {
            [typeof(PullMonitorConfiguration)] = new List<YamlDerivedType>
            {
                new YamlDerivedType(typeof(HttpMonitorConfiguration)) { Tag = "!http" },
                new YamlDerivedType(typeof(PingMonitorConfiguration)) { Tag = "!ping" },
                new YamlDerivedType(typeof(TcpMonitorConfiguration)) { Tag = "!tcp" },
            }
        }
    }
};

The base class can optionally have [YamlPolymorphic] for per-type discriminator config, but [YamlDerivedType] is no longer required.

Merge behavior

Runtime entries are additive to attribute-based registrations ([YamlDerivedType] and [JsonDerivedType]). Attribute-based entries take precedence when the same discriminator or type is registered in both.

Changes

  • YamlDerivedType.cs (new) — Runtime derived type mapping class with 3 constructors matching YamlDerivedTypeAttribute
  • YamlPolymorphismOptions.cs — Added DerivedTypeMappings dictionary property
  • YamlObjectConverter.cs — Modified PolymorphismModel.TryCreate() to merge runtime registrations with attribute-based entries
  • YamlRuntimeDerivedTypeTests.cs (new) — 31 tests covering:
    • Property discriminator (serialize/deserialize/non-first position)
    • Tag discriminator (serialize/deserialize/both modes)
    • Integer discriminators
    • Mixed attribute + runtime registration with precedence
    • Default derived types (no discriminator)
    • Unknown discriminator handling (fail/fallback)
    • Custom discriminator property names
    • Roundtrip serialization
    • Dictionary with polymorphic values
    • Cross-project architecture pattern
    • Multiple base types
    • Validation (non-assignable types, null arguments)
    • Empty mappings behavior

…phism

Add YamlDerivedType class and DerivedTypeMappings on YamlPolymorphismOptions
to enable registering polymorphic derived types at runtime, without requiring
[YamlDerivedType] attributes on the base class.

This enables cross-project polymorphism where the base type and derived types
live in different assemblies (e.g., clean architecture, plugin systems,
modular monoliths).

Runtime entries are merged with attribute-based registrations. Attribute-based
entries take precedence when the same discriminator or type is registered in
both.

Includes 31 tests covering property/tag/integer discriminators, roundtrips,
mixed attribute+runtime registration, dictionary values, default derived types,
unknown discriminator handling, and validation.
@xoofx xoofx merged commit 2a9eb64 into xoofx:main Mar 29, 2026
1 check passed
@xoofx
Copy link
Copy Markdown
Owner

xoofx commented Mar 29, 2026

Thanks! I will need to add proper support for source generator in a separate commit.

@fdcastel
Copy link
Copy Markdown
Contributor Author

Thanks! I will need to add proper support for source generator in a separate commit.

@xoofx Feel free to ask me for any changes, or share your suggestions/improvements that come to your mind.

I’m currently porting an application to SharpYaml, so I’ve got a few different use cases I’m working through. I’ll be sending more PRs today along with the context behind each one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants