Skip to content

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
stringstring
boolboolean
int / uint / long / ulong / short / ushort / byte / sbyteinteger with format: int32/uint32/int64/uint64/int16/uint16/uint8/int8 (plus min/max bounds for the common widths)
float / double / decimalnumber with format: float/double/decimal
Guidstring, format: uuid
DateTime / DateTimeOffsetstring, format: date-time (DateTimeOffset additionally carries x-rivet-csharp-type so the import round-trip recovers the exact type)
DateOnlystring, format: date
TimeOnlystring, format: time
Uristring, format: uri
byte[]string, contentEncoding: base64 (the OpenAPI 3.1 idiom — matches the System.Text.Json wire format), plus x-rivet-csharp-type
charstring 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
objectuntyped (empty) schema — "any JSON value", emitted deliberately and with no diagnostic. openapi-typescript consumers see unknown; cast at the boundary.

Composites ​

  • Records / classes → object schemas in components/schemas with properties + required.
  • Enums → integer schemas 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 into string schemas with the exact C# member names as values (no naming policy writes CLR names verbatim). One of the Rivet*EnumConverter family 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 same JsonNamingPolicy instance 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 in required; add [RivetOptional] for a property clients may omit.
  • Collections (List<T>, IReadOnlyList<T>, arrays) → array with items.
  • Dictionaries → object with additionalProperties. Non-string keys add a propertyNames schema: enum keys $ref the enum schema (which is emitted — Dictionary<Color, int> registers Color), string-backed brand keys $ref the brand schema, and string-serializable primitive keys (Guid, DateTime/DateOnly/ TimeOnly, Uri, char, numerics) emit type: string with the original format (char keys carry the length-1 bounds instead) plus x-rivet-csharp-type where the format alone is ambiguous. Unsupported key types degrade to unconstrained string keys with diagnostic RIV1013.
  • Generics are monomorphised: PagedResult<MemberDto> becomes a PagedResult_MemberDto component carrying x-rivet-generic.
  • Value-object brands: an explicit [RivetScalar] on a record/class/struct with exactly one property named Value (e.g. [RivetScalar] record Email(string Value)) lowers to its inner primitive with x-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 + discriminator with a complete tag → $ref mapping, named after the base. Each registration becomes a {Base}_{Tag} variant component: the discriminator property first (default $type, single-value enum, 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; UnknownDerivedTypeHandling has 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).