Skip to content

Commit

Permalink
Add oneOf to JSON Schema (#3557)
Browse files Browse the repository at this point in the history
Fix #3544.
  • Loading branch information
bterlson authored Jun 11, 2024
1 parent c5ca089 commit 295e68a
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/json-schema-oneof-2024-5-11-8-56-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/json-schema"
---

Add support for @oneOf decorator.
16 changes: 16 additions & 0 deletions docs/emitters/json-schema/reference/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,22 @@ Specify that the numeric type must be a multiple of some numeric value.
| ----- | ----------------- | -------------------------------------------------- |
| value | `valueof numeric` | The numeric type must be a multiple of this value. |

### `@oneOf` {#@TypeSpec.JsonSchema.oneOf}

Specify that `oneOf` should be used instead of `anyOf` for that union.

```typespec
@TypeSpec.JsonSchema.oneOf
```

#### Target

`Union | ModelProperty`

#### Parameters

None

### `@prefixItems` {#@TypeSpec.JsonSchema.prefixItems}

Specify that the target array must begin with the provided types.
Expand Down
1 change: 1 addition & 0 deletions docs/emitters/json-schema/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ npm install --save-peer @typespec/json-schema
- [`@minContains`](./decorators.md#@TypeSpec.JsonSchema.minContains)
- [`@minProperties`](./decorators.md#@TypeSpec.JsonSchema.minProperties)
- [`@multipleOf`](./decorators.md#@TypeSpec.JsonSchema.multipleOf)
- [`@oneOf`](./decorators.md#@TypeSpec.JsonSchema.oneOf)
- [`@prefixItems`](./decorators.md#@TypeSpec.JsonSchema.prefixItems)
- [`@uniqueItems`](./decorators.md#@TypeSpec.JsonSchema.uniqueItems)

Expand Down
17 changes: 17 additions & 0 deletions packages/json-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ When true, emit all references as json schema files, even if the referenced type
- [`@minContains`](#@mincontains)
- [`@minProperties`](#@minproperties)
- [`@multipleOf`](#@multipleof)
- [`@oneOf`](#@oneof)
- [`@prefixItems`](#@prefixitems)
- [`@uniqueItems`](#@uniqueitems)

Expand Down Expand Up @@ -348,6 +349,22 @@ Specify that the numeric type must be a multiple of some numeric value.
| ----- | ----------------- | -------------------------------------------------- |
| value | `valueof numeric` | The numeric type must be a multiple of this value. |

#### `@oneOf`

Specify that `oneOf` should be used instead of `anyOf` for that union.

```typespec
@TypeSpec.JsonSchema.oneOf
```

##### Target

`Union | ModelProperty`

##### Parameters

None

#### `@prefixItems`

Specify that the target array must begin with the provided types.
Expand Down
6 changes: 6 additions & 0 deletions packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Numeric,
Scalar,
Type,
Union,
} from "@typespec/compiler";

/**
Expand Down Expand Up @@ -43,6 +44,11 @@ export type BaseUriDecorator = (
*/
export type IdDecorator = (context: DecoratorContext, target: Type, id: string) => void;

/**
* Specify that `oneOf` should be used instead of `anyOf` for that union.
*/
export type OneOfDecorator = (context: DecoratorContext, target: Union | ModelProperty) => void;

/**
* Specify that the numeric type must be a multiple of some numeric value.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
$minContains,
$minProperties,
$multipleOf,
$oneOf,
$prefixItems,
$uniqueItems,
} from "@typespec/json-schema";
Expand All @@ -30,6 +31,7 @@ import type {
MinContainsDecorator,
MinPropertiesDecorator,
MultipleOfDecorator,
OneOfDecorator,
PrefixItemsDecorator,
UniqueItemsDecorator,
} from "./TypeSpec.JsonSchema.js";
Expand All @@ -38,6 +40,7 @@ type Decorators = {
$jsonSchema: JsonSchemaDecorator;
$baseUri: BaseUriDecorator;
$id: IdDecorator;
$oneOf: OneOfDecorator;
$multipleOf: MultipleOfDecorator;
$contains: ContainsDecorator;
$minContains: MinContainsDecorator;
Expand All @@ -57,6 +60,7 @@ const _: Decorators = {
$jsonSchema,
$baseUri,
$id,
$oneOf,
$multipleOf,
$contains,
$minContains,
Expand Down
5 changes: 5 additions & 0 deletions packages/json-schema/lib/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ extern dec baseUri(target: Reflection.Namespace, baseUri: valueof string);
*/
extern dec id(target: unknown, id: valueof string);

/**
* Specify that `oneOf` should be used instead of `anyOf` for that union.
*/
extern dec oneOf(target: Reflection.Union | Reflection.ModelProperty);

/**
* Specify that the numeric type must be a multiple of some numeric value.
*
Expand Down
10 changes: 10 additions & 0 deletions packages/json-schema/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
MinContainsDecorator,
MinPropertiesDecorator,
MultipleOfDecorator,
OneOfDecorator,
PrefixItemsDecorator,
UniqueItemsDecorator,
} from "../generated-defs/TypeSpec.JsonSchema.js";
Expand Down Expand Up @@ -145,6 +146,15 @@ export function getId(program: Program, target: Type) {
return program.stateMap(idKey).get(target);
}

const oneOfKey = createStateSymbol("JsonSchema.oneOf");
export const $oneOf: OneOfDecorator = (context: DecoratorContext, target: Type) => {
context.program.stateMap(oneOfKey).set(target, true);
};

export function isOneOf(program: Program, target: Type) {
return program.stateMap(oneOfKey).has(target);
}

const containsKey = createStateSymbol("JsonSchema.contains");
export const $contains: ContainsDecorator = (
context: DecoratorContext,
Expand Down
14 changes: 12 additions & 2 deletions packages/json-schema/src/json-schema-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
getPrefixItems,
getUniqueItems,
isJsonSchemaDeclaration,
isOneOf,
} from "./index.js";
import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js";
export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSchemaEmitterOptions> {
Expand Down Expand Up @@ -174,6 +175,11 @@ export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSche
result.default = this.#getDefaultValue(property.type, property.default);
}

if (result.anyOf && isOneOf(this.emitter.getProgram(), property)) {
result.oneOf = result.anyOf;
delete result.anyOf;
}

this.#applyConstraints(property, result);

return result;
Expand Down Expand Up @@ -296,17 +302,21 @@ export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSche
}

unionDeclaration(union: Union, name: string): EmitterOutput<object> {
const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf";

const withConstraints = this.#initializeSchema(union, name, {
anyOf: this.emitter.emitUnionVariants(union),
[key]: this.emitter.emitUnionVariants(union),
});

this.#applyConstraints(union, withConstraints);
return this.#createDeclaration(union, name, withConstraints);
}

unionLiteral(union: Union): EmitterOutput<object> {
const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf";

return new ObjectBuilder({
anyOf: this.emitter.emitUnionVariants(union),
[key]: this.emitter.emitUnionVariants(union),
});
}

Expand Down
20 changes: 20 additions & 0 deletions packages/json-schema/test/unions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ describe("emitting unions", () => {
assert.strictEqual(Foo["x-foo"], true);
});

it("handles oneOf decorator", async () => {
const schemas = await emitSchema(`
@oneOf
union Foo {
"a",
"b"
}
model Bar {
@oneOf
prop: "a" | "b"
}
`);

const Foo = schemas["Foo.json"];
const Bar = schemas["Bar.json"];

assert.ok(Foo.oneOf, "Foo uses oneOf");
assert.ok(Bar.properties.prop.oneOf, "Bar.prop uses oneOf");
});
it("handles decorators on variants", async () => {
const schemas = await emitSchema(`
union Foo {
Expand Down

0 comments on commit 295e68a

Please sign in to comment.