ajsc 5.2.4 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +303 -144
  3. package/dist/converter/BaseConverter.d.ts +315 -0
  4. package/dist/converter/BaseConverter.js +131 -0
  5. package/dist/converter/BaseConverter.js.map +1 -0
  6. package/dist/converter/Emitter.d.ts +35 -0
  7. package/dist/converter/Emitter.js +50 -0
  8. package/dist/converter/Emitter.js.map +1 -0
  9. package/dist/converter/discriminatedUnions.d.ts +47 -0
  10. package/dist/converter/discriminatedUnions.js +168 -0
  11. package/dist/converter/discriminatedUnions.js.map +1 -0
  12. package/dist/converter/formatDefault.d.ts +20 -0
  13. package/dist/converter/formatDefault.js +31 -0
  14. package/dist/converter/formatDefault.js.map +1 -0
  15. package/dist/converter/index.d.ts +24 -0
  16. package/dist/converter/index.js +24 -0
  17. package/dist/converter/index.js.map +1 -0
  18. package/dist/converter/mergeUnions.d.ts +36 -0
  19. package/dist/converter/mergeUnions.js +189 -0
  20. package/dist/converter/mergeUnions.js.map +1 -0
  21. package/dist/converter/naming.d.ts +29 -0
  22. package/dist/converter/naming.js +137 -0
  23. package/dist/converter/naming.js.map +1 -0
  24. package/dist/converter/registry.d.ts +18 -0
  25. package/dist/converter/registry.js +50 -0
  26. package/dist/converter/registry.js.map +1 -0
  27. package/dist/converter/walk.d.ts +9 -0
  28. package/dist/converter/walk.js +40 -0
  29. package/dist/converter/walk.js.map +1 -0
  30. package/dist/index.d.ts +71 -3
  31. package/dist/index.js +63 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/{JSONSchemaConverter.d.ts → ir/JSONSchemaConverter.d.ts} +1 -1
  34. package/dist/{JSONSchemaConverter.js → ir/JSONSchemaConverter.js} +9 -3
  35. package/dist/ir/JSONSchemaConverter.js.map +1 -0
  36. package/dist/ir/index.d.ts +1 -0
  37. package/dist/ir/index.js +2 -0
  38. package/dist/ir/index.js.map +1 -0
  39. package/dist/kotlin/KotlinBaseConverter.d.ts +18 -0
  40. package/dist/kotlin/KotlinBaseConverter.js +36 -0
  41. package/dist/kotlin/KotlinBaseConverter.js.map +1 -0
  42. package/dist/kotlin/KotlinConverter.d.ts +67 -0
  43. package/dist/kotlin/KotlinConverter.js +142 -0
  44. package/dist/kotlin/KotlinConverter.js.map +1 -0
  45. package/dist/kotlin/annotations.d.ts +26 -0
  46. package/dist/kotlin/annotations.js +35 -0
  47. package/dist/kotlin/annotations.js.map +1 -0
  48. package/dist/kotlin/enums.d.ts +15 -0
  49. package/dist/kotlin/enums.js +58 -0
  50. package/dist/kotlin/enums.js.map +1 -0
  51. package/dist/kotlin/index.d.ts +13 -0
  52. package/dist/kotlin/index.js +14 -0
  53. package/dist/kotlin/index.js.map +1 -0
  54. package/dist/kotlin/objectEmitter.d.ts +12 -0
  55. package/dist/kotlin/objectEmitter.js +74 -0
  56. package/dist/kotlin/objectEmitter.js.map +1 -0
  57. package/dist/kotlin/sealedUnion.d.ts +17 -0
  58. package/dist/kotlin/sealedUnion.js +74 -0
  59. package/dist/kotlin/sealedUnion.js.map +1 -0
  60. package/dist/kotlin/typeMapper.d.ts +17 -0
  61. package/dist/kotlin/typeMapper.js +107 -0
  62. package/dist/kotlin/typeMapper.js.map +1 -0
  63. package/dist/kotlin/unsupported.d.ts +13 -0
  64. package/dist/kotlin/unsupported.js +53 -0
  65. package/dist/kotlin/unsupported.js.map +1 -0
  66. package/dist/swift/SwiftBaseConverter.d.ts +18 -0
  67. package/dist/swift/SwiftBaseConverter.js +38 -0
  68. package/dist/swift/SwiftBaseConverter.js.map +1 -0
  69. package/dist/swift/SwiftConverter.d.ts +60 -0
  70. package/dist/swift/SwiftConverter.js +113 -0
  71. package/dist/swift/SwiftConverter.js.map +1 -0
  72. package/dist/swift/discriminatedEnum.d.ts +18 -0
  73. package/dist/swift/discriminatedEnum.js +99 -0
  74. package/dist/swift/discriminatedEnum.js.map +1 -0
  75. package/dist/swift/enums.d.ts +15 -0
  76. package/dist/swift/enums.js +62 -0
  77. package/dist/swift/enums.js.map +1 -0
  78. package/dist/swift/index.d.ts +13 -0
  79. package/dist/swift/index.js +14 -0
  80. package/dist/swift/index.js.map +1 -0
  81. package/dist/swift/structEmitter.d.ts +12 -0
  82. package/dist/swift/structEmitter.js +70 -0
  83. package/dist/swift/structEmitter.js.map +1 -0
  84. package/dist/swift/typeMapper.d.ts +18 -0
  85. package/dist/swift/typeMapper.js +106 -0
  86. package/dist/swift/typeMapper.js.map +1 -0
  87. package/dist/swift/unsupported.d.ts +19 -0
  88. package/dist/swift/unsupported.js +88 -0
  89. package/dist/swift/unsupported.js.map +1 -0
  90. package/dist/typescript/TypescriptBaseConverter.d.ts +25 -0
  91. package/dist/typescript/TypescriptBaseConverter.js +178 -0
  92. package/dist/typescript/TypescriptBaseConverter.js.map +1 -0
  93. package/dist/typescript/TypescriptConverter.d.ts +74 -0
  94. package/dist/typescript/TypescriptConverter.js +254 -0
  95. package/dist/typescript/TypescriptConverter.js.map +1 -0
  96. package/dist/typescript/index.d.ts +12 -0
  97. package/dist/typescript/index.js +13 -0
  98. package/dist/typescript/index.js.map +1 -0
  99. package/dist/utils/index.d.ts +2 -0
  100. package/dist/utils/index.js +3 -0
  101. package/dist/utils/index.js.map +1 -0
  102. package/package.json +39 -6
  103. package/dist/JSONSchemaConverter.js.map +0 -1
  104. package/dist/JSONSchemaConverter.test.d.ts +0 -1
  105. package/dist/JSONSchemaConverter.test.js +0 -585
  106. package/dist/JSONSchemaConverter.test.js.map +0 -1
  107. package/dist/Typebox.test.d.ts +0 -1
  108. package/dist/Typebox.test.js +0 -88
  109. package/dist/Typebox.test.js.map +0 -1
  110. package/dist/TypescriptBaseConverter.d.ts +0 -75
  111. package/dist/TypescriptBaseConverter.js +0 -321
  112. package/dist/TypescriptBaseConverter.js.map +0 -1
  113. package/dist/TypescriptConverter.additionalProperties.test.d.ts +0 -1
  114. package/dist/TypescriptConverter.additionalProperties.test.js +0 -110
  115. package/dist/TypescriptConverter.additionalProperties.test.js.map +0 -1
  116. package/dist/TypescriptConverter.arrays.test.d.ts +0 -1
  117. package/dist/TypescriptConverter.arrays.test.js +0 -130
  118. package/dist/TypescriptConverter.arrays.test.js.map +0 -1
  119. package/dist/TypescriptConverter.composites.advanced.test.d.ts +0 -1
  120. package/dist/TypescriptConverter.composites.advanced.test.js +0 -1070
  121. package/dist/TypescriptConverter.composites.advanced.test.js.map +0 -1
  122. package/dist/TypescriptConverter.composites.test.d.ts +0 -1
  123. package/dist/TypescriptConverter.composites.test.js +0 -335
  124. package/dist/TypescriptConverter.composites.test.js.map +0 -1
  125. package/dist/TypescriptConverter.d.ts +0 -163
  126. package/dist/TypescriptConverter.js +0 -606
  127. package/dist/TypescriptConverter.js.map +0 -1
  128. package/dist/TypescriptConverter.jsdoc.test.d.ts +0 -1
  129. package/dist/TypescriptConverter.jsdoc.test.js +0 -194
  130. package/dist/TypescriptConverter.jsdoc.test.js.map +0 -1
  131. package/dist/TypescriptConverter.objects.test.d.ts +0 -1
  132. package/dist/TypescriptConverter.objects.test.js +0 -258
  133. package/dist/TypescriptConverter.objects.test.js.map +0 -1
  134. package/dist/TypescriptConverter.options.test.d.ts +0 -1
  135. package/dist/TypescriptConverter.options.test.js +0 -501
  136. package/dist/TypescriptConverter.options.test.js.map +0 -1
  137. package/dist/TypescriptConverter.primitives.test.d.ts +0 -1
  138. package/dist/TypescriptConverter.primitives.test.js +0 -26
  139. package/dist/TypescriptConverter.primitives.test.js.map +0 -1
  140. package/dist/utils/path-utils.test.d.ts +0 -1
  141. package/dist/utils/path-utils.test.js +0 -92
  142. package/dist/utils/path-utils.test.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ All notable changes to ajsc are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project follows [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [7.0.0] — 2026-04-24
6
+
7
+ This release adds first-class **Kotlin** and **Swift** language targets, restores the root subpath import that v6 dropped, and ships several behavioral fixes to the TypeScript converter that consumers depending on the older buggy output may need to migrate.
8
+
9
+ ### Added
10
+
11
+ - **Kotlin converter** (`ajsc/kotlin`, or `emitKotlin` from `ajsc`).
12
+ - Emits idiomatic `data class` declarations.
13
+ - Default `serializer: "kotlinx"` adds `@Serializable`, `@SerialName`, `@Contextual`. Pass `serializer: "none"` for plain types.
14
+ - Discriminated `oneOf`/`anyOf` unions emit as `sealed interface` with `@JsonClassDiscriminator`.
15
+ - JVM stdlib type mapping for date-time (`java.time.Instant`), uuid (`java.util.UUID`), uri (`java.net.URI`), date / time.
16
+ - `Pair<A,B>` / `Triple<A,B,C>` for 2- and 3-element tuples.
17
+ - `packageName` option emits a `package …` line at the top of the output.
18
+ - **Swift converter** (`ajsc/swift`, or `emitSwift` from `ajsc`).
19
+ - Emits `struct` declarations with `: Codable` conformance by default.
20
+ - Foundation stdlib mapping (`Date`, `UUID`, `URL`).
21
+ - Discriminated unions emit as `enum` with associated values, including a hand-written `init(from:)` and `encode(to:)` that dispatch by the schema's discriminator field.
22
+ - `CodingKeys` blocks generated when JSON keys differ from Swift property names.
23
+ - Acronym preservation in enum case names: `case US` rather than `case uS = "US"`.
24
+ - `accessLevel` option (`"public"` default, `"internal"` available).
25
+ - **Top-level emit functions** at the package root: `emitTypescript`, `emitKotlin`, `emitSwift`, plus the shared `EmitResult` type.
26
+ - **`nameRegistry` and `namePrefix` opts** on `BaseConverterOpts`. Pass a shared `Set<string>` across multiple emit calls so types extracted from sibling schemas don't collide when concatenated into a single Kotlin `object` or Swift `enum` namespace. `namePrefix` prepends a per-slot prefix to extracted (nested) type names — `BodyAddress` vs `ResponseAddress` rather than `Address` vs `Address2`. See README "Emitting multiple schemas into one namespace" for the recommended pattern. Most useful for Kotlin and Swift; harmless for TypeScript.
27
+ - **`ajsc/converter` subpath** for downstream extension authors. Exports `BaseConverter`, `Emitter`, `walkIR`, `LanguageProfile`, `BaseConverterContext`, `RefTypeEntry`, and `BaseConverterOpts`.
28
+ - **`rootTypeName` option** on every converter — overrides the schema-derived root name without modifying the schema.
29
+ - **Introspection fields** on every converter: `rootTypeName: string`, `extractedTypeNames: string[]`, `imports: string[]`. Codegen pipelines can reference emitted type names without parsing the `code` string.
30
+ - **Architecture documentation** at `docs/architecture/README.md` — data flow, layered class hierarchy, `LanguageProfile` pattern, per-language module structure, and the steps to add a new language target.
31
+ - **Cross-language fixture corpus** of 18 schemas with 54 golden output files (TS + Kotlin + Swift) under `src/integrations/fixtures/`.
32
+ - **JSON Schema 2020-12 `prefixItems`** treated as equivalent to legacy array-form `items`.
33
+ - **Schema-level `default`** now propagated to Kotlin/Swift property declarations for primitive defaults (`val foo: String = "x"`, `let foo: Int64 = 0`).
34
+ - **Format `int32`** is now honored: emits `Int` (Kotlin) and `Int32` (Swift) where previously these always emitted `Long` / `Int64`.
35
+ - **Root-level enum schemas** (`{ type: "string", enum: [...] }`) now emit as `enum class` (Kotlin) and `public enum: String, Codable` (Swift), instead of empty data classes/structs.
36
+
37
+ ### Changed (breaking — TypeScript output)
38
+
39
+ - **`$ref: "#"` recursion in TypeScript** now emits direct self-reference. A schema like `{ "title": "Tree", "properties": { "children": { "type": "array", "items": { "$ref": "#" } } } }` now produces:
40
+ ```ts
41
+ export type Tree = { children?: Array<Tree>; };
42
+ ```
43
+ instead of the previous bifurcated form (`Child` extracted with `Array<any>` recursion). Consumers depending on the old `Child` extracted type by name will need to update.
44
+ - **`anyOf` merge** preserves the parent's `required` flag. A required schema field that gets anyOf-merged is now non-optional in output. Example:
45
+ ```ts
46
+ // before: actor?: Actor
47
+ // after: actor: Actor
48
+ ```
49
+ - **`oneOf` variant naming in TypeScript** uses discriminator-derived names. A schema with two oneOf variants `{ kind: "wrapper" }` / `{ kind: "scalar" }` now emits `WrapperOuter` / `ScalarOuter` rather than collision-suffixed `Outer` / `OuterType`. Kotlin and Swift have always produced discriminator-derived names; this brings TypeScript into alignment.
50
+
51
+ ### Changed (breaking — extension API)
52
+
53
+ - **`./converter` subpath public surface refactored.** Most consumers won't notice — only authors who subclassed `BaseConverter` to override hook methods.
54
+ - 9 separate `protected` hook methods (`shouldEraseDiscriminator`, `processOneOfAsDiscriminatedUnion`, `getDiscriminatedVariantParentName`, `shouldPopulateDiscriminatorInfo`, `detectSelfReferenceToRoot`, etc.) consolidated into a single `protected readonly languageProfile: LanguageProfile` field. See `docs/architecture/README.md` for the migration pattern.
55
+ - `RefTypes` is now `RefTypeEntry[]` (array of named records) instead of a tuple-of-tuples. Access changes from `entry[2].code` → `entry.code`, `[_s, name, { code }]` destructure → `{ name, code }`.
56
+ - Several previously-protected helper methods (`findDiscriminatorProperty`, `collectUnionPropertyNames`, `stripDiscriminatorField`, `getConstStringValue`, `findAvailableName`) are now public (with `@internal` JSDoc) so they can be referenced by helper-module Context interfaces.
57
+ - **`BaseConverterContext` interface** introduced as the surface helper modules operate on. `BaseConverter implements BaseConverterContext`. Helper modules in `src/converter/` (`registry.ts`, `naming.ts`, `mergeUnions.ts`, `discriminatedUnions.ts`, `walk.ts`) take a `BaseConverterContext` rather than the abstract class, decoupling helpers from the class hierarchy.
58
+ - **Per-language `<Lang>ConverterContext` interfaces** (`KotlinConverterContext`, `SwiftConverterContext`) extend `BaseConverterContext` with language-specific state and methods. The Kotlin and Swift converter classes `implements` their respective Context interface; helper modules in `src/kotlin/` and `src/swift/` take the interface. Replaces the prior `_x` accessor wrapper pattern with a single declarative interface.
59
+
60
+ ### Changed (non-breaking)
61
+
62
+ - **Swift discriminated-enum emission** uses consistent 4-space indentation throughout (previously had 4/8-space mismatches in `init(from:)` / `encode(to:)` blocks).
63
+ - **Kotlin module structure** split into focused files: `typeMapper.ts`, `objectEmitter.ts`, `sealedUnion.ts`, `enums.ts`, `unsupported.ts`, `annotations.ts`. Pure internal refactor; no public API change.
64
+ - **Swift module structure** split into focused files: `typeMapper.ts`, `structEmitter.ts`, `discriminatedEnum.ts`, `enums.ts`, `unsupported.ts`. Pure internal refactor.
65
+ - **`BaseConverter` split** into focused files: `walk.ts`, `naming.ts`, `registry.ts`, `mergeUnions.ts`, `discriminatedUnions.ts`. Pure internal refactor.
66
+
67
+ ### Fixed
68
+
69
+ - **Kotlin `init` reserved word** — was missing from `KOTLIN_RESERVED`; properties named `init` now correctly emit as `init_`.
70
+ - **Swift acronym case names** — `case US` instead of `case uS = "US"` for all-uppercase enum values.
71
+ - **Schema `default`** is no longer silently dropped in Kotlin and Swift output for primitive defaults.
72
+
73
+ ### Removed
74
+
75
+ - The `dart` keyword from `package.json` keywords (no Dart converter is available; misleading for npm discoverability).
76
+
77
+ ### Internal
78
+
79
+ - 90+ commits since v6.0.0. Test count grew from 188 → 473.
80
+ - New regression-net pattern: vitest snapshots of the kitchen-sink schema (`_baseline-snapshot.test.ts.snap`) catch any drift in TS converter output across refactors. Used throughout this release as the gate for the `BaseConverter` refactor.
81
+ - Cross-language golden fixtures (18 schemas × 3 languages = 54 goldens) protect against drift.
82
+ - Cross-call integration tests (`src/__tests__/cross-call-name-registry.test.ts`, `src/__tests__/multi-call-integration.test.ts`) lock in the orchestration pattern for downstream codegen pipelines.
83
+
84
+ ### Known follow-ups (deferred to a future release)
85
+
86
+ These were surfaced during v7 development but intentionally not implemented to keep the release scope focused. Real, agreed-on next steps:
87
+
88
+ - **`EmitSession` API**: an emit-session object that bundles `nameRegistry` plus `refTypes` across multiple calls so identical schema shapes (e.g., the same `address` structure in both `body` and `response`) emit as a single declaration referenced from both slots. The current `nameRegistry` fix avoids compile errors but emits structurally-identical types twice (`BodyAddress`, `ResponseAddress`). A session API would dedupe at the structural level.
89
+ - **Smart `namePrefix`**: prefix extracted names only when they would collide, instead of unconditionally. Today, `namePrefix: "Body"` produces `BodyAddress`, `BodyContact`, `BodySettings` even when only `Address` actually collides with another slot. A two-pass approach (collect names, detect overlaps, prefix the conflicting ones) would give more ergonomic output.
90
+ - **Cross-call collision fallback**: when `nameRegistry` is set without `namePrefix`, prefer numeric suffix (`Address2`) over postfix-list (`AddressType`) fallbacks. The postfix list was designed for single-call path-escalation; in cross-call mode, postfix names become semantically misleading.
91
+ - **`additionalItems` support**: tuple-form `items: [...]` plus `additionalItems: { ... }` is currently silently dropped.
92
+ - **Schema `examples` lifting**: currently dropped (no clean target idiom across all three languages).
93
+ - **`@internal` accessor consolidation**: `BaseConverter` exposes several public-with-`@internal` methods (`findDiscriminatorProperty`, `collectUnionPropertyNames`, etc.) for the `BaseConverterContext` interface. They work but the `@internal` JSDoc isn't enforced. A `Symbol`-keyed accessor pattern or a separate "internal" module would prevent downstream code from depending on them.
94
+
95
+ ---
96
+
97
+ ## [6.0.0] — earlier
98
+
99
+ Reorganized `src` by feature and switched to subpath exports. The previous root barrel (`import { TypescriptConverter } from "ajsc"`) stopped resolving in v6. (v7 restores the root entry; see [Migrating from v6 to v7](./README.md#migrating-from-v6-to-v7).)
100
+
101
+ ## [5.x] — earlier
102
+
103
+ Earlier releases. See `git log` for detail.
package/README.md CHANGED
@@ -1,198 +1,357 @@
1
- # ajsc - Another JSON Schema Parser
1
+ # ajsc Another JSON Schema Converter
2
2
 
3
- **ajsc** is an npm package that transforms JSON Schema definitions into an intermediate representation (IR) called `IRNode`. This intermediate form is designed to be consumed by language-specific converters—such as the built-in [TypeScript converter](#typescriptconverter)—or by any custom converter you create for languages like Kotlin or Swift. This library streamlines the process of converting API JSON Schema definitions into strong-typed code representations, ensuring consistency between your API contracts and client implementations.
3
+ **ajsc** transforms JSON Schema documents into idiomatic, language-native code for **TypeScript**, **Kotlin**, and **Swift**. It exposes a small function-style API for typical codegen pipelines, plus class-based converters for advanced extension.
4
4
 
5
- ---
5
+ ```bash
6
+ npm install ajsc
7
+ ```
6
8
 
7
- ## Features
9
+ ```ts
10
+ import { emitTypescript, emitKotlin, emitSwift } from "ajsc";
8
11
 
9
- - **Comprehensive Schema Parsing:**
10
- Convert JSON Schema types including strings, numbers, booleans, nulls, literals, enums, unions, intersections, arrays, and objects into a unified IR.
12
+ const schema = {
13
+ title: "User",
14
+ type: "object",
15
+ properties: {
16
+ id: { type: "integer" },
17
+ name: { type: "string" },
18
+ email: { type: "string", format: "email" },
19
+ },
20
+ required: ["id", "name"],
21
+ };
11
22
 
12
- - **Intermediate Representation (`IRNode`):**
13
- The `IRNode` provides a standard structure that captures type, constraints, and path information. This representation is ideal for further transformation into various target languages.
23
+ const ts = emitTypescript(schema);
24
+ const kt = emitKotlin(schema);
25
+ const sw = emitSwift(schema);
26
+ ```
14
27
 
15
- - **Plugin-Based Architecture:**
16
- Use the provided language plugin (currently, a [TypeScript converter](#typescriptconverter)) or write your own converter to support additional languages such as Kotlin or Swift.
28
+ Each emit function returns the same shape:
17
29
 
18
- - **$defs and References Handling:**
19
- Resolve JSON Schema definitions (`$defs`) and references (`$ref`) seamlessly, ensuring reusable schema components are properly processed.
30
+ ```ts
31
+ interface EmitResult {
32
+ code: string; // declarations only — no `import` lines
33
+ rootTypeName: string; // top-level emitted type ("User")
34
+ extractedTypeNames: string[]; // additional types emitted (nested objects, enums, variants)
35
+ imports: string[]; // language-native module/symbol paths to import
36
+ }
37
+ ```
20
38
 
21
- - **Unique Signature Tracking:**
22
- For objects and arrays, the library tracks unique signatures to avoid duplicate type definitions when converting to code.
39
+ `code` contains type declarations only. Consumers assemble the final source file by combining `imports` with `code`.
23
40
 
24
41
  ---
25
42
 
26
- ## Installation
43
+ ## Output examples
27
44
 
28
- Install **ajsc** via npm:
45
+ ### TypeScript
29
46
 
30
- ```bash
31
- npm install ajsc
47
+ ```ts
48
+ import { emitTypescript } from "ajsc";
49
+
50
+ const { code } = emitTypescript({
51
+ title: "User", type: "object",
52
+ properties: { id: { type: "integer" }, name: { type: "string" } },
53
+ required: ["id", "name"],
54
+ });
55
+
56
+ // code:
57
+ // export type User = { id: number; name: string; };
32
58
  ```
33
59
 
34
- ## Usage
60
+ ### Kotlin (default: kotlinx-serialization)
61
+
62
+ ```ts
63
+ import { emitKotlin } from "ajsc";
64
+
65
+ const { code, imports } = emitKotlin({
66
+ title: "User", type: "object",
67
+ properties: { id: { type: "integer" }, name: { type: "string" } },
68
+ required: ["id", "name"],
69
+ });
70
+
71
+ // imports: ["kotlinx.serialization.Serializable"]
72
+ // code:
73
+ // @Serializable
74
+ // data class User(
75
+ // val id: Long,
76
+ // val name: String,
77
+ // )
78
+ ```
35
79
 
36
- ### Basic Conversion to IRNode
80
+ Pass `serializer: "none"` for plain types with no annotations.
37
81
 
38
- The primary function of ajsc is to convert a JSON Schema into an IRNode. Here’s an example of converting a simple JSON Schema that defines a string type:
82
+ ### Swift (default: Codable)
39
83
 
40
- ```javascript
41
- import { JSONSchemaConverter } from "ajsc";
84
+ ```ts
85
+ import { emitSwift } from "ajsc";
42
86
 
43
- const schema = { type: "string" };
44
- const converter = new JSONSchemaConverter(schema);
87
+ const { code, imports } = emitSwift({
88
+ title: "User", type: "object",
89
+ properties: { id: { type: "integer" }, name: { type: "string" } },
90
+ required: ["id", "name"],
91
+ });
45
92
 
46
- console.log(converter.irNode);
47
- // Output:
48
- // {
49
- // type: "string",
50
- // constraints: {},
51
- // path: "",
93
+ // imports: []
94
+ // code:
95
+ // public struct User: Codable {
96
+ // public let id: Int64
97
+ // public let name: String
52
98
  // }
53
99
  ```
54
100
 
55
- Converting Complex Schemas
56
- You can also convert more complex schemas including objects, arrays, unions, intersections, and even schemas with $defs:
57
-
58
- ```javascript
59
- import { JSONSchemaConverter } from "ajsc";
60
-
61
- const complexSchema = {
62
- $defs: {
63
- Person: {
64
- type: "object",
65
- properties: {
66
- name: { type: "string" },
67
- age: { type: "number" },
68
- },
69
- required: ["name"],
70
- },
71
- },
72
- type: "object",
73
- properties: {
74
- person: { $ref: "#/$defs/Person" },
75
- },
76
- };
101
+ Pass `serializer: "none"` to drop `Codable` conformance.
77
102
 
78
- const converter = new JSONSchemaConverter(complexSchema);
79
- console.log(converter.irNode);
103
+ ### Assembling a final file
104
+
105
+ ```ts
106
+ const { code, imports } = emitKotlin(schema);
107
+ const file = [...imports.map((i) => `import ${i}`), "", code].join("\n");
80
108
  ```
81
109
 
82
- ## API Reference
110
+ Swift `imports` are module names (`["Foundation"]`); Kotlin `imports` are fully-qualified symbol paths. TypeScript `imports` is always empty.
83
111
 
84
- ### JSONSchemaConverter
112
+ ---
85
113
 
86
- - Constructor:
87
- `new JSONSchemaConverter(schema: object)`
88
- - Properties:
89
- - irNode: The resulting intermediate representation of the JSON Schema.
90
- - Supported Schema Types:
91
- - Primitive Types: string, number, boolean, null
92
- - Literals: Using the const keyword.
93
- - Enums: Using the enum property.
94
- - Unions: When type is an array (e.g., ["string", "number"]).
95
- - Intersections: Using allOf to combine multiple schemas.
96
- - Arrays: With type: "array" and an items schema.
97
- - Objects: With type: "object" and a properties definition.
98
- - $defs and $ref: For defining and referencing reusable schema parts.
114
+ ## Package layout
99
115
 
100
- ### TypescriptConverter
116
+ | Subpath | Exports |
117
+ |------------------|--------------------------------------------------------------------------------------------------------|
118
+ | `ajsc` | `emitTypescript`, `emitKotlin`, `emitSwift`, `EmitResult`, plus the converter classes (re-exported) |
119
+ | `ajsc/typescript`| `TypescriptConverter`, `TypescriptBaseConverter`, related types |
120
+ | `ajsc/kotlin` | `KotlinConverter`, `KotlinBaseConverter`, `sanitizeKotlinIdentifier`, `KOTLIN_RESERVED` |
121
+ | `ajsc/swift` | `SwiftConverter`, `SwiftBaseConverter`, `sanitizeSwiftIdentifier`, `SWIFT_RESERVED` |
122
+ | `ajsc/converter` | `BaseConverter`, `Emitter`, `walkIR`, `LanguageProfile`, `BaseConverterOpts`, `RefTypeEntry`, etc. |
123
+ | `ajsc/ir` | `JSONSchemaConverter` (JSON Schema → IRNode tree) |
124
+ | `ajsc/types` | `IRNode`, `ILanguageConverter`, signature types |
125
+ | `ajsc/utils` | `PathUtils`, `toPascalCase` |
126
+
127
+ The root (`ajsc`) is the typical entry point. Subpaths are for advanced use — extending a converter, accessing the IR layer, building a custom language target.
128
+
129
+ ---
101
130
 
102
- The package includes a TypeScript language plugin that converts the IRNode into TypeScript type definitions.
131
+ ## Function-style vs class-style
132
+
133
+ ```ts
134
+ // Function-style — recommended for most consumers.
135
+ import { emitKotlin } from "ajsc";
136
+ const result = emitKotlin(schema, opts);
137
+
138
+ // Class-style — for subclassing or accessing converter internals.
139
+ import { KotlinConverter } from "ajsc/kotlin";
140
+ const converter = new KotlinConverter(schema, opts);
141
+ console.log(converter.code, converter.imports);
142
+ ```
143
+
144
+ Both produce identical output; the function form just bundles `code`, `rootTypeName`, `extractedTypeNames`, `imports` into a single return value. For codegen pipelines, the function form is usually nicer — it avoids the `new` and gives a stable result shape.
145
+
146
+ ---
103
147
 
104
- - Constructor:
105
- `new TypescriptConverter(schema: object, options?: TypescriptConverterOpts)`
106
- - Properties:
107
- - code: A string containing the generated TypeScript code.
148
+ ## Options
108
149
 
109
- #### Options
150
+ All three converters accept `BaseConverterOpts` plus language-specific options.
151
+
152
+ ### Shared options (`BaseConverterOpts`)
110
153
 
111
154
  | Option | Type | Default | Description |
112
155
  |--------|------|---------|-------------|
113
- | `inlineTypes` | `boolean` | `false` | If true, object types are inlined instead of extracted as named type aliases. |
114
- | `depluralize` | `boolean` | `true` | Singularize array item type names. Handles irregular plurals (e.g. `entries` `Entry`, `people` `Person`). |
115
- | `arrayItemNaming` | `string \| false` | `false` | Controls the postfix for array item type names. `false` = no postfix, `string` = custom postfix (e.g. `"Item"` → `ContactItem`). |
116
- | `enumStyle` | `"union" \| "enum"` | `"union"` | `"union"` emits `"a" \| "b"`, `"enum"` emits `export enum`. Only applies when `inlineTypes` is false and all values are strings. |
117
- | `uncountableWords` | `string[]` | `undefined` | Additional words that should not be singularized (built-in: `"data"`, `"metadata"`). |
156
+ | `rootTypeName` | `string` | `schema.title \|\| "Root"` | Override the top-level type name. |
157
+ | `arrayItemNaming` | `string \| false` | `false` | Postfix for array-item type names. `false` = property name, `"Item"` = `ContactsItem`. |
158
+ | `depluralize` | `boolean` | `true` | Singularize array-item path segments (`entries` → `Entry`). |
159
+ | `uncountableWords` | `string[]` | `[]` | Words to skip when singularizing. Built-ins: `data`, `metadata`. |
160
+ | `unsupportedUnions` | `"throw" \| "fallback"` | `"throw"` | What to do with untagged unions: throw with a path-bearing error, or emit a language fallback type (`JsonElement` / `AnyCodable` / `any`). |
118
161
 
119
- #### Usage Examples
162
+ ### TypeScript (`TypescriptConverterOpts`)
120
163
 
121
- Default behavior array item types are automatically singularized:
164
+ | Option | Type | Default | Description |
165
+ |--------|------|---------|-------------|
166
+ | `inlineTypes` | `boolean` | `false` | If true, nested object types are inlined rather than extracted to named aliases. |
167
+ | `enumStyle` | `"union" \| "enum"` | `"union"` | `"union"` emits `"a" \| "b" \| "c"`; `"enum"` emits `export enum Status { ... }`. |
168
+ | `jsdoc` | `boolean` | `false` | Emit JSDoc comments from JSON Schema `description` and `title`. Requires `inlineTypes: false`. |
122
169
 
123
- ```javascript
124
- import { TypescriptConverter } from "ajsc";
170
+ ### Kotlin (`KotlinConverterOpts`)
125
171
 
126
- const schema = {
127
- type: "object",
128
- properties: {
129
- name: { type: "string" },
130
- age: { type: "number" },
131
- contacts: {
132
- type: "array",
133
- items: {
134
- type: "object",
135
- properties: {
136
- email: { type: "string" },
137
- },
138
- required: ["email"],
139
- },
140
- },
141
- profile: {
142
- type: "object",
143
- properties: {
144
- email: { type: "string" },
145
- },
146
- required: ["email"],
147
- },
148
- },
149
- required: ["name", "age"],
150
- };
172
+ | Option | Type | Default | Description |
173
+ |--------|------|---------|-------------|
174
+ | `serializer` | `"kotlinx" \| "none"` | `"kotlinx"` | Emit `@Serializable`/`@SerialName`/`@Contextual` annotations and matching imports. `"none"` emits plain types. |
175
+ | `packageName` | `string` | `undefined` | If set, emit `package <name>` at the top of the output. |
151
176
 
152
- const tsConverter = new TypescriptConverter(schema);
153
- console.log(tsConverter.code);
177
+ ### Swift (`SwiftConverterOpts`)
154
178
 
155
- // Output:
156
- // export type Contact = { email: string; };
157
- // export type Profile = { email: string; };
158
- //
159
- // export type Root = { name: string; age: number; contacts?: Array<Contact>; profile?: Profile; };
179
+ | Option | Type | Default | Description |
180
+ |--------|------|---------|-------------|
181
+ | `serializer` | `"codable" \| "none"` | `"codable"` | Emit `: Codable` conformance, `CodingKeys` enums, and discriminated-union `init(from:)`/`encode(to:)` plumbing. `"none"` emits plain types. |
182
+ | `accessLevel` | `"public" \| "internal"` | `"public"` | Access modifier on emitted types and members. |
183
+
184
+ ---
185
+
186
+ ## Type mapping
187
+
188
+ ### TypeScript
189
+
190
+ JSON Schema → idiomatic TS. `string` → `string`, `integer` → `number`, etc. Discriminated `oneOf`/`anyOf` → tagged union types. `$ref: "#"` → direct self-reference.
191
+
192
+ ### Kotlin (target: JVM)
193
+
194
+ | JSON Schema | Kotlin | Notes |
195
+ |-------------|--------|-------|
196
+ | `string` | `String` | |
197
+ | `string, format: date-time` | `java.time.Instant` | `@Contextual` under kotlinx |
198
+ | `string, format: date` / `time` | `java.time.LocalDate` / `LocalTime` | `@Contextual` under kotlinx |
199
+ | `string, format: uuid` | `java.util.UUID` | `@Contextual` under kotlinx |
200
+ | `string, format: uri` | `java.net.URI` | `@Contextual` under kotlinx |
201
+ | `string, format: email` / `hostname` / `ipv4` / `ipv6` | `String` + KDoc note | |
202
+ | `integer` | `Long` | `format: int32` → `Int` |
203
+ | `number` | `Double` | |
204
+ | `boolean` | `Boolean` | |
205
+ | `array` | `List<T>` | |
206
+ | `object` | `data class` | `@Serializable` under kotlinx |
207
+ | `enum` (string) | `enum class` | `@Serializable` under kotlinx |
208
+ | `oneOf` w/ discriminator | `sealed interface` | `@JsonClassDiscriminator` under kotlinx |
209
+ | Tuple length 2 / 3 | `Pair<A,B>` / `Triple<A,B,C>` | length > 3 throws |
210
+
211
+ ### Swift
212
+
213
+ | JSON Schema | Swift | Notes |
214
+ |-------------|-------|-------|
215
+ | `string` | `String` | |
216
+ | `string, format: date-time` | `Foundation.Date` | requires `JSONDecoder.dateDecodingStrategy = .iso8601` |
217
+ | `string, format: uuid` | `Foundation.UUID` | |
218
+ | `string, format: uri` | `Foundation.URL` | |
219
+ | `string, format: email` etc. | `String` + `///` doc note | |
220
+ | `integer` | `Int64` | `format: int32` → `Int32` |
221
+ | `number` | `Double` | |
222
+ | `boolean` | `Bool` | |
223
+ | `array` | `[T]` | |
224
+ | `object` | `struct` | `: Codable` by default |
225
+ | `enum` (string) | `enum: String, Codable` | raw-value enum |
226
+ | `oneOf` w/ discriminator | `enum` w/ associated values | hand-rolled `init(from:)`/`encode(to:)` |
227
+ | Tuple (homogeneous) | `[T]` | |
228
+ | Tuple (heterogeneous, codable) | throws | use `serializer: "none"` for native tuples |
229
+
230
+ Schema-level `default` values are emitted inline for primitive types in Kotlin/Swift (`val foo: String = "x"`, `let foo: Int64 = 0`).
231
+
232
+ ---
233
+
234
+ ## Emitting multiple schemas into one namespace
235
+
236
+ Codegen pipelines that emit several sibling schemas into a shared output (e.g., wrapping an endpoint's `pathParams`, `query`, `body`, `response`, and error types under a single Kotlin `object` or Swift `enum`) need to avoid duplicate-name collisions across the emit calls. Each emit call has its own private name-tracking state by default, so two slots with a nested `address: { type: "object" }` would each emit a `data class Address(...)` — a duplicate-class compile error in the merged output.
237
+
238
+ Pass a shared `nameRegistry: Set<string>` across calls. The converter uses it as its declaration registry and mutates it as new types are emitted. Pair with `namePrefix` for clean per-slot names:
239
+
240
+ ```ts
241
+ import { emitKotlin } from "ajsc";
242
+
243
+ const registry = new Set<string>();
244
+
245
+ const body = emitKotlin(bodySchema, {
246
+ nameRegistry: registry,
247
+ namePrefix: "Body",
248
+ rootTypeName: "Body",
249
+ });
250
+
251
+ const response = emitKotlin(responseSchema, {
252
+ nameRegistry: registry,
253
+ namePrefix: "Response",
254
+ rootTypeName: "Response",
255
+ });
256
+
257
+ // body.code: `data class Body(...)` + `data class BodyAddress(...)`
258
+ // response.code: `data class Response(...)` + `data class ResponseAddress(...)`
259
+ // Concatenated under one `object Endpoint { ... }` — no name collisions.
160
260
  ```
161
261
 
162
- Inline types (no extracted type aliases):
262
+ ### Without `namePrefix`
263
+
264
+ You can pass `nameRegistry` alone — collisions still resolve correctly via the standard fallback path. But the resulting names are awkward:
163
265
 
164
- ```javascript
165
- const tsConverter = new TypescriptConverter(schema, { inlineTypes: true });
166
- console.log(tsConverter.code);
266
+ ```ts
267
+ const registry = new Set<string>();
268
+ emitKotlin(bodySchema, { nameRegistry: registry, rootTypeName: "Body" });
269
+ emitKotlin(responseSchema, { nameRegistry: registry, rootTypeName: "Response" });
167
270
 
168
- // Output:
169
- // { name: string; age: number; contacts?: Array<{ email: string; }>; profile?: { email: string; }; }
271
+ // body emits: `data class Address(...)` ← bare name (first-come-first-served)
272
+ // response emits: `data class AddressType(...)` ← postfix fallback (semantically wrong)
170
273
  ```
171
274
 
172
- Enum style:
275
+ `AddressType` is a literal type name in the emitted Kotlin/Swift — but it's not a "type of address," it's just another `Address`. The fallback exists because path-collision escalation was designed for single-call use; in cross-call scenarios where every slot's path looks the same, the fallback fires immediately. **Recommendation: always pair `nameRegistry` with a per-slot `namePrefix`.** That's the pattern shown above and the one the integration tests assert against.
173
276
 
174
- ```javascript
175
- const enumSchema = {
176
- type: "object",
177
- properties: {
178
- status: { type: "string", enum: ["active", "inactive", "pending"] },
179
- },
180
- };
277
+ ### Notes
278
+
279
+ `namePrefix` does **not** affect the root type name (use `rootTypeName` for that). It only applies to extracted nested types, and it's applied unconditionally — every nested type gets the prefix, not just colliding ones. That can produce slightly verbose names for unique types (e.g. `BodyContact` even when no other slot has a `Contact`), but it's predictable and avoids the alternative's two-pass complexity.
280
+
281
+ Both `nameRegistry` and `namePrefix` work for all three languages. They're most useful for Kotlin and Swift, which require named declarations for non-primitive types. TypeScript supports them too but rarely needs them — most codegen pipelines use `inlineTypes: true` to flatten nested types instead.
282
+
283
+ ---
284
+
285
+ ## Working with the IR directly
181
286
 
182
- const tsConverter = new TypescriptConverter(enumSchema, { enumStyle: "enum" });
183
- console.log(tsConverter.code);
287
+ If you need the intermediate representation (e.g., to write a custom emitter), use `JSONSchemaConverter`:
184
288
 
185
- // Output:
186
- // export enum Status { Active = "active", Inactive = "inactive", Pending = "pending" }
187
- //
188
- // export type Root = { status?: Status; };
289
+ ```ts
290
+ import { JSONSchemaConverter } from "ajsc/ir";
291
+
292
+ const ir = new JSONSchemaConverter({ type: "string" }).irNode;
293
+ // { type: "string", path: "", constraints: {} }
189
294
  ```
190
295
 
191
- Custom uncountable words:
296
+ `IRNode` shape is documented in `ajsc/types`.
192
297
 
193
- ```javascript
194
- const tsConverter = new TypescriptConverter(schema, {
195
- uncountableWords: ["criteria", "alumni"],
196
- });
197
- // "criteria" array items will produce type "Criteria", not "Criterium"
298
+ ## Building a custom language emitter
299
+
300
+ To target a new language, extend `BaseConverter`:
301
+
302
+ ```ts
303
+ import { BaseConverter, type LanguageProfile } from "ajsc/converter";
304
+
305
+ class DartConverter extends BaseConverter {
306
+ protected readonly languageProfile: LanguageProfile = {
307
+ language: "dart",
308
+ // ... per-language overrides
309
+ };
310
+ // ... implement generateObjectType and the emission orchestration
311
+ }
312
+ ```
313
+
314
+ See `docs/architecture/README.md` for the layered design — `BaseConverter`, the `LanguageProfile` pattern, and the per-language helper module conventions used by Kotlin and Swift.
315
+
316
+ ---
317
+
318
+ ## Migrating from v6 to v7
319
+
320
+ ### Breaking changes
321
+
322
+ These are output changes for existing consumers. They're behavioral fixes, not regressions, but if you've been depending on the older shapes you'll see diffs:
323
+
324
+ - **`$ref: "#"` recursion** in TypeScript now emits direct self-reference (`Array<Tree>`) rather than a buggy `Child` extraction with `Array<any>` recursion.
325
+ - **`anyOf` merge** preserves the parent's `required` flag. A required schema field that gets anyOf-merged is now non-optional in output (`actor: Actor` instead of `actor?: Actor`).
326
+ - **`oneOf` variant naming** in TypeScript now uses discriminator-derived names (`WrapperOuter`, `ScalarOuter`) rather than path-collision suffix names (`Outer`, `OuterType`). Kotlin and Swift always used the discriminator-derived form; this brings TS into alignment.
327
+ - **`format: int32`** is now honored: emits `Int` (Kotlin) and `Int32` (Swift) where previously these always emitted `Long`/`Int64`. `format: int64` and unset format both still emit `Long`/`Int64`.
328
+ - **Schema-level `default`** is now emitted in Kotlin and Swift property declarations for primitive defaults (`val foo: String = "x"`). TS output is unchanged.
329
+ - **Swift acronym preservation**: enum case names for all-uppercase tokens (`US`, `URL`, `ID`) are no longer lowerCamelCased (`uS`/`uRL`/`iD`). They're preserved as-is.
330
+ - **`./converter` extension API** has been heavily refactored. If you subclassed `BaseConverter` to override hook methods, see the new `LanguageProfile` pattern in `docs/architecture/README.md`. Most legacy hook overrides should migrate to a single `protected readonly languageProfile` field.
331
+
332
+ ### New features
333
+
334
+ - **Kotlin and Swift converters.** First-class language targets, not just IR.
335
+ - **Top-level emit functions.** `emitTypescript`, `emitKotlin`, `emitSwift` from `"ajsc"`.
336
+ - **`./converter` subpath** for downstream extension authors.
337
+ - **Root subpath restored.** `import { TypescriptConverter } from "ajsc"` works again (was subpath-only in v6).
338
+
339
+ ### Migrating v5 → v7
340
+
341
+ If you're still on v5, the v6 → v7 changes apply on top of these:
342
+
343
+ ```diff
344
+ - import { TypescriptConverter, JSONSchemaConverter } from "ajsc";
345
+ + import { TypescriptConverter } from "ajsc"; // root entry restored
346
+ + import { JSONSchemaConverter } from "ajsc/ir"; // moved to subpath in v6
198
347
  ```
348
+
349
+ ---
350
+
351
+ ## Architecture
352
+
353
+ For contributors and downstream extension authors, see [`docs/architecture/README.md`](./docs/architecture/README.md) — the data flow, layered class hierarchy, `LanguageProfile` pattern, per-language module structure, and the steps to add a new language target.
354
+
355
+ ## License
356
+
357
+ MIT