Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add oneOf to JSON Schema #3557

Merged
merged 5 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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
Loading
Loading