diff --git a/.chronus/changes/json-schema-oneof-2024-5-11-8-56-5.md b/.chronus/changes/json-schema-oneof-2024-5-11-8-56-5.md new file mode 100644 index 00000000000..ef96db35aea --- /dev/null +++ b/.chronus/changes/json-schema-oneof-2024-5-11-8-56-5.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/json-schema" +--- + +Add support for @oneOf decorator. \ No newline at end of file diff --git a/docs/emitters/json-schema/reference/decorators.md b/docs/emitters/json-schema/reference/decorators.md index a61dd90562f..e8445e5ad45 100644 --- a/docs/emitters/json-schema/reference/decorators.md +++ b/docs/emitters/json-schema/reference/decorators.md @@ -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. diff --git a/docs/emitters/json-schema/reference/index.mdx b/docs/emitters/json-schema/reference/index.mdx index bbc10f06d42..ce0f343fb01 100644 --- a/docs/emitters/json-schema/reference/index.mdx +++ b/docs/emitters/json-schema/reference/index.mdx @@ -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) diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md index 9abdc9d89e5..17a93bfaaa9 100644 --- a/packages/json-schema/README.md +++ b/packages/json-schema/README.md @@ -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) @@ -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. diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index b65738bc4aa..e33657fcc02 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts @@ -5,6 +5,7 @@ import type { Numeric, Scalar, Type, + Union, } from "@typespec/compiler"; /** @@ -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. * diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts index 8e2778f71f1..80ecdef7f8e 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts @@ -13,6 +13,7 @@ import { $minContains, $minProperties, $multipleOf, + $oneOf, $prefixItems, $uniqueItems, } from "@typespec/json-schema"; @@ -30,6 +31,7 @@ import type { MinContainsDecorator, MinPropertiesDecorator, MultipleOfDecorator, + OneOfDecorator, PrefixItemsDecorator, UniqueItemsDecorator, } from "./TypeSpec.JsonSchema.js"; @@ -38,6 +40,7 @@ type Decorators = { $jsonSchema: JsonSchemaDecorator; $baseUri: BaseUriDecorator; $id: IdDecorator; + $oneOf: OneOfDecorator; $multipleOf: MultipleOfDecorator; $contains: ContainsDecorator; $minContains: MinContainsDecorator; @@ -57,6 +60,7 @@ const _: Decorators = { $jsonSchema, $baseUri, $id, + $oneOf, $multipleOf, $contains, $minContains, diff --git a/packages/json-schema/lib/main.tsp b/packages/json-schema/lib/main.tsp index fae17c91d93..3aa30c31f0d 100644 --- a/packages/json-schema/lib/main.tsp +++ b/packages/json-schema/lib/main.tsp @@ -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. * diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index de5c4bdced7..27a23b8defc 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -29,6 +29,7 @@ import { MinContainsDecorator, MinPropertiesDecorator, MultipleOfDecorator, + OneOfDecorator, PrefixItemsDecorator, UniqueItemsDecorator, } from "../generated-defs/TypeSpec.JsonSchema.js"; @@ -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, diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index 36da5fc205b..ee99b954df6 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -71,6 +71,7 @@ import { getPrefixItems, getUniqueItems, isJsonSchemaDeclaration, + isOneOf, } from "./index.js"; import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js"; export class JsonSchemaEmitter extends TypeEmitter, JSONSchemaEmitterOptions> { @@ -174,6 +175,11 @@ export class JsonSchemaEmitter extends TypeEmitter, 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; @@ -296,8 +302,10 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } unionDeclaration(union: Union, name: string): EmitterOutput { + 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); @@ -305,8 +313,10 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } unionLiteral(union: Union): EmitterOutput { + const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf"; + return new ObjectBuilder({ - anyOf: this.emitter.emitUnionVariants(union), + [key]: this.emitter.emitUnionVariants(union), }); } diff --git a/packages/json-schema/test/unions.test.ts b/packages/json-schema/test/unions.test.ts index 34256aca1bb..e022be0ecd4 100644 --- a/packages/json-schema/test/unions.test.ts +++ b/packages/json-schema/test/unions.test.ts @@ -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 {