Skip to content

Polymorphic Types in GraphQL

Verified by tests

PolymorphicTypeExtensionsTests — library CI run #31657041675 (2026-08-13)

Whizbang provides extension methods to register polymorphic type hierarchies with HotChocolate, enabling turn-key GraphQL support for types using [JsonPolymorphic] and [JsonDerivedType] attributes.

Overview

The PolymorphicTypeExtensions class enables:

  • Automatic Type Registration - Base type becomes GraphQL interface, derived types become implementations
  • Attribute Discovery - Derived types discovered from [JsonDerivedType] attributes
  • Consistent Serialization - Same type hierarchy works for JSON API and GraphQL
  • Startup-Time Discovery - The auto-discovery overload reads [JsonPolymorphic]/[JsonDerivedType] attribute metadata once at registration; nothing is discovered per request

Installation

Installation

dotnet add package Whizbang.Transports.HotChocolate

Defining Polymorphic Types

Base Type with Attributes

Base Type with Attributes

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(TextFieldSettings), "text")]
[JsonDerivedType(typeof(NumberFieldSettings), "number")]
[JsonDerivedType(typeof(DateFieldSettings), "date")]
public abstract class AbstractFieldSettings {
    public string Label { get; init; } = "";
    public bool Required { get; init; }
}

public class TextFieldSettings : AbstractFieldSettings {
    public int? MaxLength { get; init; }
    public string? Placeholder { get; init; }
}

public class NumberFieldSettings : AbstractFieldSettings {
    public decimal? MinValue { get; init; }
    public decimal? MaxValue { get; init; }
    public int DecimalPlaces { get; init; }
}

public class DateFieldSettings : AbstractFieldSettings {
    public DateOnly? MinDate { get; init; }
    public DateOnly? MaxDate { get; init; }
    public string Format { get; init; } = "yyyy-MM-dd";
}

Registration Methods

Auto-Discovery

Automatically discovers derived types from [JsonDerivedType] attributes:

Auto-Discovery

builder.Services.AddGraphQLServer()
    .AddWhizbangLenses()
    .AddPolymorphicType<AbstractFieldSettings>();

Explicit Registration

Manually specify derived types:

Explicit Registration

builder.Services.AddGraphQLServer()
    .AddWhizbangLenses()
    .AddPolymorphicType<AbstractFieldSettings>(
        typeof(TextFieldSettings),
        typeof(NumberFieldSettings),
        typeof(DateFieldSettings));

Generated GraphQL Schema

The registration generates:

AbstractFieldSettings

interface AbstractFieldSettings {
  label: String!
  required: Boolean!
}

type TextFieldSettings implements AbstractFieldSettings {
  label: String!
  required: Boolean!
  maxLength: Int
  placeholder: String
}

type NumberFieldSettings implements AbstractFieldSettings {
  label: String!
  required: Boolean!
  minValue: Decimal
  maxValue: Decimal
  decimalPlaces: Int!
}

type DateFieldSettings implements AbstractFieldSettings {
  label: String!
  required: Boolean!
  minDate: Date
  maxDate: Date
  format: String!
}

Querying Polymorphic Types

Fragment Spread


{
  formFields {
    nodes {
      settings {
        label
        required
        ... on TextFieldSettings {
          maxLength
          placeholder
        }
        ... on NumberFieldSettings {
          minValue
          maxValue
          decimalPlaces
        }
        ... on DateFieldSettings {
          minDate
          maxDate
          format
        }
      }
    }
  }
}

Inline Fragments


{
  formFields {
    nodes {
      settings {
        __typename
        label
        required
        ... on TextFieldSettings { maxLength }
        ... on NumberFieldSettings { decimalPlaces }
      }
    }
  }
}

Complete Example

Model Definitions

Model Definitions

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(EmailNotification), "email")]
[JsonDerivedType(typeof(SmsNotification), "sms")]
[JsonDerivedType(typeof(PushNotification), "push")]
public abstract class NotificationSettings {
    public bool Enabled { get; init; }
}

public class EmailNotification : NotificationSettings {
    public string EmailAddress { get; init; } = "";
    public bool IncludeAttachments { get; init; }
}

public class SmsNotification : NotificationSettings {
    public string PhoneNumber { get; init; } = "";
}

public class PushNotification : NotificationSettings {
    public string DeviceToken { get; init; } = "";
    public bool ShowBadge { get; init; }
}

Service Registration

Service Registration

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGraphQLServer()
    .AddWhizbangLenses()
    .AddPolymorphicType<NotificationSettings>()
    .AddQueryType<Query>();

var app = builder.Build();
app.MapGraphQL();
app.Run();

Query Type

Query Type

public class Query {
    public NotificationSettings GetUserNotificationSettings(
        [Service] IUserService userService,
        Guid userId) {
        return userService.GetNotificationSettings(userId);
    }
}

GraphQL Query


{
  userNotificationSettings(userId: "...") {
    __typename
    enabled
    ... on EmailNotification {
      emailAddress
      includeAttachments
    }
    ... on SmsNotification {
      phoneNumber
    }
    ... on PushNotification {
      deviceToken
      showBadge
    }
  }
}

Error Handling

Missing JsonPolymorphic Attribute

Missing JsonPolymorphic Attribute

// This will throw InvalidOperationException
builder.Services.AddGraphQLServer()
    .AddPolymorphicType<SomeTypeWithoutAttribute>();
// Error: "Type 'SomeTypeWithoutAttribute' must have [JsonPolymorphic] attribute to use AddPolymorphicType."

Missing JsonDerivedType Attributes

Missing JsonDerivedType Attributes

[JsonPolymorphic]
public abstract class BaseType { } // No [JsonDerivedType] attributes

// This will throw InvalidOperationException
builder.Services.AddGraphQLServer()
    .AddPolymorphicType<BaseType>();
// Error: "Type 'BaseType' must have at least one [JsonDerivedType] attribute to use AddPolymorphicType."

Best Practices

  1. Use Consistent Discriminators - Keep $type discriminator consistent between JSON API and GraphQL
  2. Prefer Auto-Discovery - Let the extension discover types from attributes to keep registration DRY
  3. Explicit for Flexibility - Use explicit registration when you need to expose different types to GraphQL than JSON
  4. Test Both APIs - Verify polymorphic types work in both REST/JSON and GraphQL contexts