Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions JsonSubTypes.Text.Json/JsonSubtypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@

[RequiresUnreferencedCode("JsonSubtypes uses reflection to discover sub-types and properties.")]
[RequiresDynamicCode("JsonSubtypes requires dynamic code for runtime type creation.")]
/// <summary>
/// A JSON converter that deserializes a polymorphic hierarchy from a discriminator property.
/// The concrete subtype is resolved from an explicit mapping (<see cref="KnownSubTypeAttribute"/>)
/// or, when no mapping is declared, by matching the discriminator string against a type name.
/// </summary>
/// <remarks>
/// <para>
/// Name-based resolution (used only when no <see cref="KnownSubTypeAttribute"/> mapping is
/// declared) instantiates the type whose name matches the discriminator, provided it is
/// assignable from the polymorphic base type and lives in the base type's assembly or in an
/// assembly registered via <see cref="JsonSubTypesTypeResolution"/>. Any such type present in
/// those assemblies can be instantiated with attacker-controlled JSON.
/// </para>
/// <para>
/// Do not expose a name-based hierarchy to untrusted JSON without validating the payload
/// upstream; prefer an explicit <see cref="KnownSubTypeAttribute"/> mapping whenever the
/// discriminator can come from outside your own code.
/// </para>
/// </remarks>
public class JsonSubtypes<T> : JsonConverter<T>, IJsonSubtypes where T : class
{
private static readonly ConcurrentDictionary<Type, Action<Utf8JsonWriter, object, JsonSerializerOptions>>
Expand Down Expand Up @@ -154,17 +173,17 @@
}
}

public override bool CanConvert(Type objectType)

Check warning on line 176 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.

Check warning on line 176 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.

Check warning on line 176 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.
{
return objectType == typeof(T);
}

public override T? Read(ref Utf8JsonReader reader, Type objectType, JsonSerializerOptions serializer)

Check warning on line 181 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.

Check warning on line 181 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'serializer' to 'options' to match the base class declaration.

Check warning on line 181 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.

Check warning on line 181 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'serializer' to 'options' to match the base class declaration.

Check warning on line 181 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'objectType' to 'typeToConvert' to match the base class declaration.
{
return ReadJson(ref reader, objectType, serializer);
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions serializer)

Check warning on line 186 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'serializer' to 'options' to match the base class declaration.

Check warning on line 186 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Rename parameter 'serializer' to 'options' to match the base class declaration.
{
if (value is null)
{
Expand Down Expand Up @@ -275,7 +294,7 @@
writer.WriteStartObject();
writer.WritePropertyName(discriminatorName);
writer.WriteRawValue(discriminatorJson, skipInputValidation: true);
foreach (JsonProperty property in document.RootElement.EnumerateObject())

Check warning on line 297 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Loops should be simplified using the "Where" LINQ method

Check warning on line 297 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Loops should be simplified using the "Where" LINQ method
{
if (!property.NameEquals(discriminatorName))
{
Expand All @@ -287,7 +306,7 @@
else
{
writer.WriteStartObject();
foreach (JsonProperty property in document.RootElement.EnumerateObject())

Check warning on line 309 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Loops should be simplified using the "Where" LINQ method
{
if (!property.NameEquals(discriminatorName))
{
Expand Down Expand Up @@ -585,7 +604,7 @@
return targetType;
}

private IJsonSubtypes? GetTypeResolver(TypeInfo? targetType, IEnumerable<IJsonSubtypes> jsonConverterCollection)

Check warning on line 607 in JsonSubTypes.Text.Json/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.
{
if (targetType == null)
{
Expand Down
18 changes: 18 additions & 0 deletions JsonSubTypes/JsonSubtypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

/// <summary>
/// A JSON converter that deserializes a polymorphic hierarchy from a discriminator property.
/// The concrete subtype is resolved from an explicit mapping (<see cref="KnownSubTypeAttribute"/>)
/// or, when no mapping is declared, by matching the discriminator string against a type name.
/// </summary>
/// <remarks>
/// <para>
/// Name-based resolution (used only when no <see cref="KnownSubTypeAttribute"/> mapping is
/// declared) instantiates the type whose name matches the discriminator, provided it is
/// assignable from the polymorphic base type and lives in the base type's assembly. Any such
/// type present in that assembly can be instantiated with attacker-controlled JSON.
/// </para>
/// <para>
/// Do not expose a name-based hierarchy to untrusted JSON without validating the payload
/// upstream; prefer an explicit <see cref="KnownSubTypeAttribute"/> mapping whenever the
/// discriminator can come from outside your own code.
/// </para>
/// </remarks>
public class JsonSubtypes : JsonConverter
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
Expand Down Expand Up @@ -290,7 +308,7 @@
return targetType;
}

private JsonSubtypes GetTypeResolver(TypeInfo targetType, IEnumerable<JsonSubtypes> jsonConverterCollection)

Check warning on line 311 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.

Check warning on line 311 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.

Check warning on line 311 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.

Check warning on line 311 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.

Check warning on line 311 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Make 'GetTypeResolver' a static method.
{
if (targetType == null)
{
Expand Down Expand Up @@ -339,7 +357,7 @@
typesFound.Add(matchingKnownType);
}

if (typesFound.Count > 1)

Check warning on line 360 in JsonSubTypes/JsonSubtypes.cs

View workflow job for this annotation

GitHub Actions / analysis

Change this condition so that it does not always evaluate to 'False'. Some code paths are unreachable.
{
throw new JsonSerializationException(
"Ambiguous type resolution, expected only one type but got: " +
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ __JsonSubTypes__ is a discriminated Json sub-type Converter implementation for .
[![CodeFactor](https://www.codefactor.io/repository/github/manuc66/JsonSubTypes/badge)](https://www.codefactor.io/repository/github/manuc66/JsonSubTypes)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmanuc66%2FJsonSubTypes?ref=badge_shield)

> **Note:** this library is built around `Json.NET`/`Newtonsoft.Json` — that is where its API and reputation come from, and the `JsonSubTypes` NuGet package targets it. A `System.Text.Json` port exists as the `JsonSubTypes.Text.Json` package (`.NET 8+`): it shares the same API but is **experimental**. Full documentation, differences and known limitations are in the dedicated section at the bottom: [System.Text.Json variant](#systemtextjson-variant).
## Which package? State and choices

`JsonSubTypes` exists in two packages that share the same API and registration model (attributes and `JsonSubtypesConverterBuilder`):

- **`JsonSubTypes`** — for `Newtonsoft.Json`, the original and stable package.
- **`JsonSubTypes.Text.Json`** (`.NET 8+`) — for `System.Text.Json`. **Experimental**: the API is complete and the code fully tested, but the stable `1.0.0` release is still pending.

The examples below use the Newtonsoft.Json package; the API is the same for `System.Text.Json`, so read them either way. If you are targeting `System.Text.Json`, then after these examples jump to the [System.Text.Json variant](#systemtextjson-variant) section, which explains the engines available there (`Build()` converter, `BuildResolver()`, AOT generator) and their differences and limitations.

> **Security:** unless a subtype mapping is explicitly declared, the converter resolves subtypes by *name* from the JSON discriminator (only types assignable from the base are considered). See the [security section](#security) before exposing a name-based hierarchy to untrusted JSON.

## DeserializeObject with custom type property name

Expand Down Expand Up @@ -363,9 +371,15 @@ public interface IExpression { }
- 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.
- **Security**: name-based subtype resolution (`GetTypeByName`, used when no `[KnownSubType]` mapping is declared) resolves a type name from the JSON discriminator against the base type's assembly (and any assembly registered via `JsonSubTypesTypeResolution`). Only types assignable from the base can be resolved, but do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream.
- **Security**: see the [security section](#security) at the bottom of this section. It applies to both packages; the only difference is the set of assemblies searched for a name-based hit.
- The property-presence builder (`JsonSubtypesWithPropertyConverterBuilder`) registers subtypes by property name, so two subtypes cannot share the same property name through the builder (use `[KnownSubTypeWithProperty]` attributes for that case).

### Security

When a subtype is resolved by *name* — which happens for both packages **only when no subtype mapping is declared at all** (no `[KnownSubType]` attribute, no `RegisterSubtype` builder call) — the converter turns the JSON discriminator string into a type name and instantiates the matching type. Declaring a mapping at all switches the converter to that mapping, even when no entry matches; the name-based path is never used then.

Only types assignable from the polymorphic base type can be resolved, but any such type present in the base type's assembly (for Newtonsoft.Json) or in that assembly plus any assembly registered via `JsonSubTypesTypeResolution` (for `System.Text.Json`) can be instantiated with attacker-controlled JSON. Do **not** expose a name-based hierarchy to untrusted JSON without validating the payload upstream; prefer explicit `[KnownSubType]` or builder mappings whenever the discriminator can come from outside your own code.

### Which engine should I use?

`JsonSubTypes.Text.Json` ships three engines that share the same configuration layer (the attributes and `JsonSubtypesConverterBuilder`), and a parity test battery keeps them aligned:
Expand Down
Loading