Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kotlin Generator Configuration

The kotlin-okhttp backend keeps sealed classes and typed data-class variants for untagged unions, selecting a member before invoking its existing Gson codec.

[kotlin-okhttp]
package_name = "com.example.sdk"
untagged_union_strategy = "firstMatch"
PolicyBehavior
firstMatch (default)First structural match in authored order; concrete members precede opaque fallbacks.
strictSame typed API; reject multiple authored concrete matches before decoding. Use a fallback only if no concrete member matches.
rawExplicit VariantRaw(JsonElement) API, preserving known, ambiguous, unmatched, and null JSON without typed selection.

Typed policies reject unmatched non-null values. Selection uses required wire keys, finite scalar constraints (including numeric and boolean enums), nested arrays/maps, and productive recursion. Numeric matching compares exact decimal significands and unbounded exponents, without rounding through Double or imposing Gson’s decimal-accessor scale cap. Recursive predicates cache both successful and failed checks by JSON-node identity. Matching is not full JSON Schema validation: format bounds remain with the existing native codecs.

After selection, the adapter invokes the original member’s codec once. Native decode errors propagate; they do not trigger another candidate or raw fallback. Writing uses the declared payload type, so a nested sealed union uses its own adapter instead of reflecting its runtime Variant* wrapper as a value0 field. Both typed policies retain the existing root-null behavior: nonnullable unions return Kotlin null, while nullable unions use VariantNull.

Kotlin typealias declarations remain transparent. Intersections retain their existing wrapper fields (for example, leftPart and rightPart); this is not an allOf codec redesign. Ordinary objects, aliases, enum declarations, and tagged unions keep their existing APIs/codecs. Explicit dictionary and mixed-object additional properties have the representations described below.

Raw mode uses Gson’s native tree adapter and temporarily enables writer null serialization, restoring its prior value even on failure. This preserves null entries inside raw trees without changing ordinary model null-field policy. An enclosing model named VariantRaw uses VariantRawVariant for its raw case.

The policy can also be set through the CLI:

openapi-nexus generate --input api.yaml --output sdk --generators kotlin-okhttp \
  --generator-config kotlin-okhttp.untagged_union_strategy=strict

Pure dictionary models

A named object with no named properties and an explicit additionalProperties schema generates a data class with additionalProperties: MutableMap<String, V> storage, defaulting to an empty map. Its Gson adapter reads and writes a flat JSON object: the storage property’s name is not added to the wire representation.

val labels = Labels()
labels.additionalProperties["keep_key"] = "value"
val json = Gson().toJson(labels) // {"keep_key":"value"}

Keys stay verbatim. Values retain their declared model, scalar, nullable or container types; additionalProperties: true and {} use JsonElement values to preserve arbitrary JSON, null entries and large numeric literals. Dictionary aliases remain transparent and containing fields, lists, maps and unions use the same adapter. JSON null at the root stays Kotlin null; non-object roots fail instead of becoming empty dictionaries.

Dictionary entry nulls are preserved without changing the writer’s surrounding null policy. Values decode from the original JSON reader through their declared Gson codecs, preserving native decode errors as well as ordinary model null-field omission. Authored model types remain distinct from Gson helper types, even when they share names such as TypeToken or JsonElement. Mixed-object preservation is described below; allOf wrapper redesign remains separate.

Mixed-object additional properties

An object with named fields and explicit additionalProperties gets a typed var additionalProperties: MutableMap<String, V> as its final defaulted primary constructor property. Each instance starts with an independent empty map.

val value = MixedMap(knownField = "fixed")
value.additionalProperties["keep_key"] = NestedValue(wireValue = "nested")
// Gson writes {"known_field":"fixed","keep_key":{"wire_value":"nested"}}
val duplicate = value.copy() // Retains the same map, as a normal shallow copy.

Existing named constructor arguments and component positions remain unchanged; an overload retains the old Java named-field constructor entrypoint. The appended storage participates in generated copy, equality, hash and component operations. Adding a data-class property changes generated binary signatures: regenerate and recompile consumers rather than mixing old and new SDK binaries.

The storage field is transient to Gson’s ordinary delegate, and the flat-object adapter handles it separately. Extension keys stay verbatim. A storage-name clash adds a numeric suffix such as additionalProperties2, without renaming authored members. An extension key equal to a declared wire key raises IllegalArgumentException on writing, even if that named field is omitted.

Opaque values (true or {}) use JsonElement to retain null entries and large numeric literals. Declared models, enums, containers, nullable values, recursive references and transparent aliases keep their native value codecs; union values retain the configured selection policy. The actual configured Gson instance continues to own named fields, field naming and custom value adapters.

Native streaming/tree coercion differences, configured numeric writing and ordinary null omission are retained. Extension entry nulls do not force null fields inside ordinary model values or alter the surrounding writer’s policy, including on failure. This support uses the tree reader/writer implementation classes from the unchanged Gson 2.11.0 dependency; native consumers cover both input modes.

Pure-dictionary codecs are unchanged. Closed/implicit objects, general inline composition lowering, opaque named-alias codecs, multi-member allOf, tagged wrapper extension storage and generation formatting remain separate concerns.