Skip to content

Commit 9aa0c76

Browse files
committed
Add cross-project polymorphism mappings
1 parent 5b27044 commit 9aa0c76

File tree

11 files changed

+1306
-110
lines changed

11 files changed

+1306
-110
lines changed

readme.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Tomlyn is a high-performance .NET [TOML](https://toml.io/en/) 1.1 parser, round-
1313
- **`System.Text.Json`-style API**: familiar surface with `TomlSerializer`, `TomlSerializerOptions`, `TomlTypeInfo<T>`
1414
- **TOML 1.1.0 only**: Tomlyn v1 targets [TOML 1.1.0](https://toml.io/en/v1.1.0) and does **not** support TOML 1.0
1515
- **Source generation**: NativeAOT / trimming friendly via `TomlSerializerContext` and `[TomlSerializable]` roots
16+
- **Cross-project polymorphism**: register derived types at runtime or on a source-generated context when base and derived types live in different assemblies
1617
- **`System.Text.Json` attribute interop**: reuse `[JsonPropertyName]`, `[JsonIgnore]`, `[JsonRequired]`, `[JsonConstructor]`, `[JsonObjectCreationHandling]`, and polymorphism attributes
1718
- **Flexible collection input**: opt a collection member into accepting either a single TOML value or an array via `[TomlSingleOrArray]`
1819
- **Allocation-free parsing pipeline**: incremental `TomlLexer``TomlParser` with precise spans for errors
@@ -114,6 +115,47 @@ var config = TomlSerializer.Deserialize(toml, MyTomlContext.Default.MyConfig);
114115
var tomlOut = TomlSerializer.Serialize(config, MyTomlContext.Default.MyConfig);
115116
```
116117

118+
### Cross-Project Polymorphism
119+
120+
When a base type lives in one project and derived types live in another, you can register derived types without putting `[TomlDerivedType]` on the base type.
121+
122+
**Reflection path**:
123+
124+
```csharp
125+
using Tomlyn;
126+
127+
var options = new TomlSerializerOptions
128+
{
129+
PolymorphismOptions = new TomlPolymorphismOptions
130+
{
131+
TypeDiscriminatorPropertyName = "kind",
132+
DerivedTypeMappings = new Dictionary<Type, IReadOnlyList<TomlDerivedType>>
133+
{
134+
[typeof(Animal)] =
135+
[
136+
new(typeof(Cat), "cat"),
137+
new(typeof(Dog), "dog"),
138+
],
139+
},
140+
},
141+
};
142+
```
143+
144+
**Source generation path**:
145+
146+
```csharp
147+
using Tomlyn.Serialization;
148+
149+
[TomlSerializable(typeof(Animal))]
150+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Cat), "cat")]
151+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Dog), "dog")]
152+
internal partial class MyTomlContext : TomlSerializerContext
153+
{
154+
}
155+
```
156+
157+
Use `TomlPolymorphismOptions.DerivedTypeMappings` and `[TomlDerivedTypeMapping]` additively with existing base-type attributes. Base-type registrations still take precedence when the same discriminator or derived type is registered more than once.
158+
117159
### Single Value Or Array Collections
118160

119161
```csharp

site/docs/serialization.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ The extension data member should be a dictionary type with string keys (e.g. `ID
457457

458458
## Polymorphism
459459

460-
Tomlyn supports discriminator-based polymorphism via attributes or options.
460+
Tomlyn supports discriminator-based polymorphism via base-type attributes, reflection-time runtime mappings, and source-generated context mappings.
461461

462462
### Attribute-based
463463

@@ -509,6 +509,51 @@ public abstract class Animal { /* ... */ }
509509
> [!NOTE]
510510
> When both Toml and Json polymorphic attributes are present on the same type, the Toml-specific attributes take precedence.
511511
512+
### Cross-project runtime mappings
513+
514+
Use [`TomlPolymorphismOptions.DerivedTypeMappings`](xref:Tomlyn.TomlPolymorphismOptions.DerivedTypeMappings) when the base type and derived types live in different projects and you're using the reflection resolver:
515+
516+
```csharp
517+
using Tomlyn;
518+
519+
var options = new TomlSerializerOptions
520+
{
521+
PolymorphismOptions = new TomlPolymorphismOptions
522+
{
523+
TypeDiscriminatorPropertyName = "kind",
524+
DerivedTypeMappings = new Dictionary<Type, IReadOnlyList<TomlDerivedType>>
525+
{
526+
[typeof(Animal)] =
527+
[
528+
new(typeof(Cat), "cat"),
529+
new(typeof(Dog), "dog"),
530+
],
531+
},
532+
},
533+
};
534+
```
535+
536+
This is especially useful for clean architecture and plugin-style applications where the base type cannot reference every concrete implementation.
537+
538+
### Cross-project source generation mappings
539+
540+
Use [`TomlDerivedTypeMappingAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeMappingAttribute) on a [`TomlSerializerContext`](xref:Tomlyn.Serialization.TomlSerializerContext) when you want the same pattern in NativeAOT / trimming-safe source-generated code:
541+
542+
```csharp
543+
using Tomlyn.Serialization;
544+
545+
[TomlSerializable(typeof(Animal))]
546+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Cat), "cat")]
547+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Dog), "dog")]
548+
internal partial class AnimalContext : TomlSerializerContext
549+
{
550+
}
551+
```
552+
553+
Mapped derived types are discovered automatically by the generator, so they do not also need their own `[TomlSerializable]` roots.
554+
555+
If the base type doesn't declare [`TomlPolymorphicAttribute`](xref:Tomlyn.Serialization.TomlPolymorphicAttribute) or a JSON equivalent, Tomlyn still supports the mapping and falls back to [`TomlPolymorphismOptions`](xref:Tomlyn.TomlPolymorphismOptions) for the discriminator property name and unknown-derived-type handling.
556+
512557
### Default derived type
513558

514559
Register one derived type **without a discriminator** to act as the default. When a TOML table has no discriminator key (or an unknown discriminator), it deserializes as the default type. When serializing the default type, no discriminator is emitted.
@@ -593,6 +638,13 @@ The priority chain is:
593638
2. `JsonPolymorphicAttribute.UnknownDerivedTypeHandling` (mapped from `JsonUnknownDerivedTypeHandling`)
594639
3. [`TomlPolymorphismOptions.UnknownDerivedTypeHandling`](xref:Tomlyn.TomlPolymorphismOptions.UnknownDerivedTypeHandling) (global default)
595640

641+
Derived type registrations are merged additively with this precedence:
642+
643+
1. [`TomlDerivedTypeAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeAttribute) on the base type
644+
2. [`JsonDerivedTypeAttribute`](xref:System.Text.Json.Serialization.JsonDerivedTypeAttribute) on the base type
645+
3. [`TomlDerivedTypeMappingAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeMappingAttribute) on a source-generated context
646+
4. [`TomlPolymorphismOptions.DerivedTypeMappings`](xref:Tomlyn.TomlPolymorphismOptions.DerivedTypeMappings) at runtime
647+
596648
> [!NOTE]
597649
> [`TomlUnknownDerivedTypeHandling.Unspecified`](xref:Tomlyn.TomlUnknownDerivedTypeHandling.Unspecified) is a sentinel value for attribute properties and **cannot** be used on [`TomlPolymorphismOptions`](xref:Tomlyn.TomlPolymorphismOptions) - doing so throws `ArgumentOutOfRangeException`.
598650

site/docs/source-generation.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ Nested types referenced by the root are discovered transitively - you only need
3838
Set [`TomlSerializableAttribute.TypeInfoPropertyName`](xref:Tomlyn.Serialization.TomlSerializableAttribute.TypeInfoPropertyName) when you want to customize the generated property name exposed by the context.
3939
Source-generated deserialization also supports C# `init` and `required` members.
4040

41+
## Cross-project polymorphism
42+
43+
When a polymorphic base type cannot reference all of its derived types, register the derived types on the context instead of on the base type:
44+
45+
```csharp
46+
using Tomlyn.Serialization;
47+
48+
[TomlSerializable(typeof(Animal))]
49+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Cat), "cat")]
50+
[TomlDerivedTypeMapping(typeof(Animal), typeof(Dog), "dog")]
51+
internal partial class AnimalContext : TomlSerializerContext
52+
{
53+
}
54+
```
55+
56+
The generator automatically includes the mapped derived types, so they don't need separate `[TomlSerializable]` roots.
57+
If the base type already has [`TomlDerivedTypeAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeAttribute) or [`JsonDerivedTypeAttribute`](xref:System.Text.Json.Serialization.JsonDerivedTypeAttribute) registrations, those take precedence over context-level mappings.
58+
4159
## Use generated metadata
4260

4361
Use the generated [`TomlTypeInfo<T>`](xref:Tomlyn.TomlTypeInfo`1) property directly (recommended):
@@ -108,6 +126,7 @@ The source generator supports these attributes at compile time:
108126
| [`JsonPropertyNameAttribute`](xref:System.Text.Json.Serialization.JsonPropertyNameAttribute) / [`TomlPropertyNameAttribute`](xref:Tomlyn.Serialization.TomlPropertyNameAttribute) | [`JsonConstructorAttribute`](xref:System.Text.Json.Serialization.JsonConstructorAttribute) / [`TomlConstructorAttribute`](xref:Tomlyn.Serialization.TomlConstructorAttribute) |
109127
| [`JsonIgnoreAttribute`](xref:System.Text.Json.Serialization.JsonIgnoreAttribute) / [`TomlIgnoreAttribute`](xref:Tomlyn.Serialization.TomlIgnoreAttribute) | [`JsonPolymorphicAttribute`](xref:System.Text.Json.Serialization.JsonPolymorphicAttribute) / [`TomlPolymorphicAttribute`](xref:Tomlyn.Serialization.TomlPolymorphicAttribute) |
110128
| [`JsonIncludeAttribute`](xref:System.Text.Json.Serialization.JsonIncludeAttribute) / [`TomlIncludeAttribute`](xref:Tomlyn.Serialization.TomlIncludeAttribute) | [`JsonDerivedTypeAttribute`](xref:System.Text.Json.Serialization.JsonDerivedTypeAttribute) / [`TomlDerivedTypeAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeAttribute) |
129+
| | [`TomlDerivedTypeMappingAttribute`](xref:Tomlyn.Serialization.TomlDerivedTypeMappingAttribute) |
111130
| [`JsonObjectCreationHandlingAttribute`](xref:System.Text.Json.Serialization.JsonObjectCreationHandlingAttribute) | [`JsonObjectCreationHandlingAttribute`](xref:System.Text.Json.Serialization.JsonObjectCreationHandlingAttribute) |
112131
| [`JsonPropertyOrderAttribute`](xref:System.Text.Json.Serialization.JsonPropertyOrderAttribute) / [`TomlPropertyOrderAttribute`](xref:Tomlyn.Serialization.TomlPropertyOrderAttribute) | |
113132
| [`JsonRequiredAttribute`](xref:System.Text.Json.Serialization.JsonRequiredAttribute) / [`TomlRequiredAttribute`](xref:Tomlyn.Serialization.TomlRequiredAttribute) | |

0 commit comments

Comments
 (0)