Type Mapping
How C# types lower into OpenAPI 3.1 schemas. Property names camelCase by default ([JsonPropertyName] overrides). Every object property is required unless it is marked [RivetOptional] or [JsonIgnore(Condition = WhenWritingNull | WhenWritingDefault)]. Nullability does not make a property optional: System.Text.Json writes null, so a string? property is required with type ["string", "null"]. Query, header and form parameters follow the model binder instead: a nullable parameter is optional.
Primitives
| C# | JSON Schema |
|---|---|
string | string |
bool | boolean |
int / uint / long / ulong / short / ushort / byte / sbyte | integer with format: int32/uint32/int64/uint64/int16/uint16/uint8/int8 (plus min/max bounds for the common widths) |
float / double / decimal | number with format: float/double/decimal |
Guid | string, format: uuid |
DateTime / DateTimeOffset | string, format: date-time (DateTimeOffset additionally carries x-rivet-csharp-type so the import round-trip recovers the exact type) |
DateOnly | string, format: date |
TimeOnly | string, format: time |
Uri | string, format: uri |
byte[] | string, contentEncoding: base64 (the OpenAPI 3.1 idiom — matches the System.Text.Json wire format), plus x-rivet-csharp-type |
char | string with minLength: 1 and maxLength: 1 (System.Text.Json writes a single-character JSON string), plus x-rivet-csharp-type so the import round-trip recovers char |
object | untyped (empty) schema — "any JSON value", emitted deliberately and with no diagnostic. openapi-typescript consumers see unknown; cast at the boundary. |
Composites
- Records / classes →
objectschemas incomponents/schemaswithproperties+required. - Enums →
integerschemas by default, matching ordinary System.Text.Json serialization of unannotated enums ({ Draft, Open }→ values 0 and 1). A type-level[JsonConverter(typeof(JsonStringEnumConverter<T>))]opts the enum intostringschemas with the exact C# member names as values (no naming policy writes CLR names verbatim). One of theRivet*EnumConverterfamily members (RivetLowerCaseEnumConverter<T>,RivetCamelCaseEnumConverter<T>,RivetSnakeCaseEnumConverter<T>,RivetKebabCaseEnumConverter<T>) opts into a cased string union — the casing lives in the converter class name, and the emitted contract values equal the runtime serializer's wire values by construction (the sameJsonNamingPolicyinstance drives both). A per-member[JsonStringEnumMemberName]overrides the converter's casing. When the casing produces the same wire value for two members (RIV1106), generation fails — loudly, never with a wrong string union. - Nullable members (
string?,int?) → 3.1 type arrays ("type": ["string", "null"]); nullable$refs use a null branch. They stay inrequired; add[RivetOptional]for a property clients may omit. - Collections (
List<T>,IReadOnlyList<T>, arrays) →arraywithitems. - Dictionaries →
objectwithadditionalProperties. Non-string keys add apropertyNamesschema: enum keys$refthe enum schema (which is emitted —Dictionary<Color, int>registersColor), string-backed brand keys$refthe brand schema, and string-serializable primitive keys (Guid,DateTime/DateOnly/TimeOnly,Uri,char, numerics) emittype: stringwith the originalformat(charkeys carry the length-1 bounds instead) plusx-rivet-csharp-typewhere the format alone is ambiguous. Unsupported key types degrade to unconstrained string keys with diagnosticRIV1013. - Generics are monomorphised:
PagedResult<MemberDto>becomes aPagedResult_MemberDtocomponent carryingx-rivet-generic. - Value-object brands: an explicit
[RivetScalar]on a record/class/struct with exactly one property namedValue(e.g.[RivetScalar] record Email(string Value)) lowers to its inner primitive withx-rivet-brand—{ "type": "string", "x-rivet-brand": "Email" }. On the wire it is just the primitive. Without[RivetScalar]the same record is an ordinary object schema (the shape alone no longer decides). - Polymorphic hierarchies (
[JsonPolymorphic]/[JsonDerivedType]on a base type) →oneOf+discriminatorwith a complete tag →$refmapping, named after the base. Each registration becomes a{Base}_{Tag}variant component: the discriminator property first (default$type, single-valueenum, required), then the derived type's full flattened property surface — matching System.Text.Json's wire output when serializing as the base type. A derived type referenced directly keeps its own untagged schema (STJ writes no discriminator for it). Non-string tags (RIV1014) and registration-less[JsonPolymorphic](RIV1015) fall back to plain flattening, loudly;UnknownDerivedTypeHandlinghas no spec representation (RIV1016).
Constraint and metadata flow
DataAnnotations and Rivet attributes enrich property schemas: minLength, maxLength, pattern, minimum/maximum, exclusiveMinimum/exclusiveMaximum, multipleOf, minItems/maxItems/uniqueItems, description, default, examples, deprecated, readOnly/writeOnly, format. See Attributes for which attribute produces which keyword — and note these are spec-only; Rivet does not enforce them at runtime (Runtime Validation).
