OpenAPI Contract Generation
Shiny Mediator can generate HTTP request contracts and response types directly from an OpenAPI (Swagger) specification. This eliminates the need to manually write contracts for APIs that already have an OpenAPI document.
Overview
Section titled “Overview”If your API already has an OpenAPI spec, you don’t need to hand-write IHttpRequest<T> contracts for every endpoint. Instead, add a <MediatorHttp> MSBuild item to your project and Shiny Mediator’s source generator will produce all the contracts and response types at build time.
-
Ensure
Shiny.Mediatoris installed in your project -
Add a
<MediatorHttp>item to your.csprojfile pointing to your OpenAPI documentFrom a local file:
<ItemGroup><MediatorHttp Include="OpenApiDoc.json"Namespace="MyApp.Api"ContractPostfix="HttpRequest"Visible="false" /></ItemGroup>From a remote URI:
<ItemGroup><MediatorHttp Include="OpenApiRemote"Uri="https://api.myapp.com/swagger/v1/swagger.json"Namespace="MyApp.Api"ContractPostfix="HttpRequest"Visible="false" /></ItemGroup> -
Register the generated client in your service configuration
builder.Services.AddShinyMediator(x => x.AddGeneratedOpenApiClient()); -
Build your project - contracts are generated automatically
-
Use the generated contracts like any other HTTP request
var result = await mediator.Request(new GetUsersHttpRequest{Page = 1,PageSize = 25});
MSBuild Attributes Reference
Section titled “MSBuild Attributes Reference”| Attribute | Required | Description |
|---|---|---|
Include |
Yes | The local file path to the OpenAPI document, or a placeholder name when using Uri |
Uri |
No | A remote URL to fetch the OpenAPI document from. When set, Include is still required but serves as an identifier |
Namespace |
Yes | The C# namespace for the generated contracts |
ContractPrefix |
No | Text prepended to each generated contract name |
ContractPostfix |
No | Text appended to each generated contract name (e.g. "HttpRequest") |
Visible |
No | Set to "false" to hide the item from the IDE solution explorer. Recommended when using Uri |
GenerateJsonConverters |
No | Set to "true" to source-generate JSON converters for AOT scenarios. See Configuration & AOT |
UseEnumNames |
No | Set to "true" to name numeric enum members from the x-enumNames (NSwag) or x-enum-varnames (AutoRest) extension and add [Flags] when x-enumFlags: true. Off by default: members are Value1, ValueMinus1, … See Type Mapping |
Example Configuration
Section titled “Example Configuration”<ItemGroup> <!-- Local OpenAPI file --> <MediatorHttp Include="specs/products-api.json" Namespace="MyApp.Products" ContractPrefix="Products" ContractPostfix="HttpRequest" Visible="false" />
<!-- Remote OpenAPI file --> <MediatorHttp Include="UsersApi" Uri="https://api.myapp.com/swagger/v1/swagger.json" Namespace="MyApp.Users" ContractPostfix="HttpRequest" Visible="false" /></ItemGroup>Discriminated (Polymorphic) Models
Section titled “Discriminated (Polymorphic) Models”Schemas with a discriminator are generated as a class hierarchy. Nothing needs to be turned on. The generator recognizes the two shapes servers actually emit:
- Union style (ASP.NET Core
Microsoft.AspNetCore.OpenApi,[JsonPolymorphic]/[JsonDerivedType]): the base declaresoneOf/anyOfof$refs plus adiscriminator. - Inheritance style (NSwag, Swashbuckle
UseAllOfForInheritance): the base declares thediscriminatorand derived schemas useallOf: [{ $ref: Base }, { ...own properties }]. Multi-level hierarchies (Puppy : Dog : Animal) are supported.
Animal: type: object discriminator: propertyName: kind mapping: dog: '#/components/schemas/Dog' properties: kind: { type: string } name: { type: string }Dog: allOf: - $ref: '#/components/schemas/Animal' - type: object properties: breed: { type: string }Cat: # not in the mapping - its discriminator value is its schema name, "Cat" allOf: - $ref: '#/components/schemas/Animal' - type: object properties: lives: { type: integer }generates
[JsonConverter(typeof(AnimalJsonConverter))]public abstract partial class Animal{ [JsonPropertyName("kind")] public abstract string Kind { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }}
public partial class Dog : Animal{ [JsonPropertyName("kind")] public override string Kind { get; set; } = "dog";
[JsonPropertyName("breed")] public string? Breed { get; set; }}
public partial class Cat : Animal { /* Kind = "Cat", Lives */ }So List<Animal> deserializes into Dog/Cat instances, and new Dog() serializes with "kind": "dog".
- The generated
AnimalJsonConverterreads the discriminator wherever it sits in the object, then hands the payload to the variant’s ownJsonTypeInfo. It works with plain reflection and withGenerateJsonConverters="true"(AOT), where it is registered in the generated resolver. - The discriminator is always written as the first property, so a strict reader on the other side (System.Text.Json without
AllowOutOfOrderMetadataProperties) accepts it. - Values missing from
discriminator.mappinguse the schema name, as the OpenAPI spec defines. $type(ASP.NET Core’s default discriminator) becomes aTypeproperty.- Bases are always
abstract. A payload with a missing or unknown discriminator throws aJsonException. A mapping entry that points at the base itself is ignored with aSHINYMED009warning, as is a schema claimed by two bases (C# has single inheritance).
Type Mapping
Section titled “Type Mapping”| OpenAPI schema | C# type |
|---|---|
integer / int32 |
int |
integer / int64 |
long |
number / float |
float |
number / double (or no format) |
double |
number / decimal |
decimal |
string / date-time |
DateTimeOffset |
string / date |
DateOnly |
string / time |
TimeOnly |
string / uuid |
Guid |
string / byte or binary |
byte[] (base64 in JSON) |
array |
List<T> |
Numeric Enums
Section titled “Numeric Enums”Integer enums generate a C# enum serialized as numbers (JsonNumberEnumConverter). An enum whose values don’t fit in int gets long as its underlying type. In path, query and header values a numeric enum is sent as its number.
The schema only carries the values, so members are named Value0, Value1, ValueMinus1. Servers built with NSwag or AutoRest also emit the C# names as an extension. Set UseEnumNames="true" to use them:
<MediatorHttp Include="./specs/orders.json" Namespace="MyApp.Orders" UseEnumNames="true" />"OrderStatus": { "type": "integer", "enum": [0, 1, 2], "x-enumNames": ["Pending", "InProgress", "Shipped"]}public enum OrderStatus{ Pending = 0, InProgress = 1, Shipped = 2,}x-enum-varnames (AutoRest) is read when x-enumNames is absent, and x-enumFlags: true adds [Flags]. If the name list doesn’t match the values one-to-one, the generator falls back to Value{n}. The switch is off by default because turning it on renames the members of any enum your spec already annotates.
Notes and Limitations
Section titled “Notes and Limitations”- When using the
Uriattribute, you must still set theIncludevalue to satisfy the MSBuild ItemGroup requirements. UseVisible="false"to hide it from the IDE solution explorer. - The
OperationIdis used to derive the contract class name when present; otherwise the name is derived from the HTTP method and path segments (see the note under Overview). Meaningful operation IDs still produce the clearest names. - Response and request-body schemas that are defined inline (an anonymous
objectrather than a$refto a named component schema) are given a synthesized, strongly-typed contract (e.g.GetUsersResponse) instead of falling back to a weakly-typedobject.


