Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [TypeScript Generator Configuration](ts_config.md)
- [Python Generator Configuration](python_config.md)
- [Java Generator Configuration](java_config.md)
- [Kotlin Generator Configuration](kotlin_config.md)
- [Architecture](architecture.md)
- [Adding a Generator](adding_a_generator.md)
- [IR Reference](ir_reference.md)
Expand Down
49 changes: 49 additions & 0 deletions docs/src/kotlin_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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.

```toml
[kotlin-okhttp]
package_name = "com.example.sdk"
untagged_union_strategy = "firstMatch"
```

| Policy | Behavior |
| --- | --- |
| `firstMatch` (default) | First structural match in authored order; concrete members precede opaque fallbacks. |
| `strict` | Same typed API; reject multiple authored concrete matches before decoding. Use a fallback only if no concrete member matches. |
| `raw` | Explicit `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. Dictionary and additional-property repair
is outside this change.

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:

```sh
openapi-nexus generate --input api.yaml --output sdk --generators kotlin-okhttp \
--generator-config kotlin-okhttp.untagged_union_strategy=strict
```
1 change: 1 addition & 0 deletions src/generators/kotlin/okhttp/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ impl KotlinOkhttpCodeGenerator {
&package_name,
&header,
&request_inputs,
self.config.untagged_union_strategy,
)?);

files.extend(sigil_emit_api::generate_api_files(
Expand Down
36 changes: 36 additions & 0 deletions src/generators/kotlin/okhttp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ use serde::{Deserialize, Serialize};
pub struct KotlinOkhttpConfig {
#[serde(default)]
pub package_name: Option<String>,
#[serde(default)]
pub untagged_union_strategy: UntaggedUnionStrategy,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UntaggedUnionStrategy {
#[default]
FirstMatch,
Strict,
Raw,
}

impl From<toml::value::Table> for KotlinOkhttpConfig {
Expand All @@ -14,3 +25,28 @@ impl From<toml::value::Table> for KotlinOkhttpConfig {
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn policies_are_explicit_and_default_to_first_match() {
assert_eq!(
KotlinOkhttpConfig::default().untagged_union_strategy,
UntaggedUnionStrategy::FirstMatch
);
for (value, policy) in [
("firstMatch", UntaggedUnionStrategy::FirstMatch),
("strict", UntaggedUnionStrategy::Strict),
("raw", UntaggedUnionStrategy::Raw),
] {
let config: KotlinOkhttpConfig =
toml::from_str(&format!("untagged_union_strategy = {value:?}")).unwrap();
assert_eq!(config.untagged_union_strategy, policy);
}
assert!(
toml::from_str::<KotlinOkhttpConfig>("untagged_union_strategy = 'unknown'").is_err()
);
}
}
Loading
Loading