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

Json Schema: Add support for simple defaults #2657

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/json-schema",
"comment": "Add support for simple literal default on model properties",
"type": "none"
}
],
"packageName": "@typespec/json-schema"
}
65 changes: 59 additions & 6 deletions packages/json-schema/src/json-schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BooleanLiteral,
compilerAssert,
emitFile,
Enum,
EnumMember,
Expand All @@ -19,6 +20,8 @@ import {
getRelativePathFromDirectory,
getSummary,
IntrinsicType,
isArrayModelType,
isNullType,
Model,
ModelProperty,
NumericLiteral,
Expand Down Expand Up @@ -64,7 +67,7 @@ import {
isJsonSchemaDeclaration,
JsonSchemaDeclaration,
} from "./index.js";
import { JSONSchemaEmitterOptions } from "./lib.js";
import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js";
export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSchemaEmitterOptions> {
#seenIds = new Set();
#typeForSourceFile = new Map<SourceFile<any>, JsonSchemaDeclaration>();
Expand Down Expand Up @@ -160,16 +163,66 @@ export class JsonSchemaEmitter extends TypeEmitter<Record<string, any>, JSONSche
}

modelPropertyLiteral(property: ModelProperty): EmitterOutput<object> {
const result = this.emitter.emitTypeReference(property.type);
const propertyType = this.emitter.emitTypeReference(property.type);

if (result.kind !== "code") {
if (propertyType.kind !== "code") {
throw new Error("Unexpected non-code result from emit reference");
}

const withConstraints = new ObjectBuilder(result.value);
this.#applyConstraints(property, withConstraints);
const result = new ObjectBuilder(propertyType.value);

return withConstraints;
if (property.default) {
result.default = this.#getDefaultValue(property.type, property.default);
}

this.#applyConstraints(property, result);

return result;
}

#getDefaultValue(type: Type, defaultType: Type): any {
const program = this.emitter.getProgram();

switch (defaultType.kind) {
case "String":
return defaultType.value;
case "Number":
return defaultType.value;
case "Boolean":
return defaultType.value;
case "Tuple":
compilerAssert(
type.kind === "Tuple" || (type.kind === "Model" && isArrayModelType(program, type)),
"setting tuple default to non-tuple value"
);

if (type.kind === "Tuple") {
return defaultType.values.map((defaultTupleValue, index) =>
this.#getDefaultValue(type.values[index], defaultTupleValue)
);
} else {
return defaultType.values.map((defaultTuplevalue) =>
this.#getDefaultValue(type.indexer!.value, defaultTuplevalue)
);
}

case "Intrinsic":
return isNullType(defaultType)
? null
: reportDiagnostic(program, {
code: "invalid-default",
format: { type: defaultType.kind },
target: defaultType,
});
case "EnumMember":
return defaultType.value ?? defaultType.name;
default:
reportDiagnostic(program, {
code: "invalid-default",
format: { type: defaultType.kind },
target: defaultType,
});
}
}

booleanLiteral(boolean: BooleanLiteral): EmitterOutput<object> {
Expand Down
11 changes: 9 additions & 2 deletions packages/json-schema/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTypeSpecLibrary, JSONSchemaType } from "@typespec/compiler";
import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";

export type FileType = "yaml" | "json";
export type Int64Strategy = "string" | "number";
Expand Down Expand Up @@ -82,7 +82,14 @@ export const EmitterOptionsSchema: JSONSchemaType<JSONSchemaEmitterOptions> = {

export const libDef = {
name: "@typespec/json-schema",
diagnostics: {},
diagnostics: {
"invalid-default": {
severity: "error",
messages: {
default: paramMessage`Invalid type '${"type"}' for a default value`,
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<JSONSchemaEmitterOptions>,
},
Expand Down
71 changes: 70 additions & 1 deletion packages/json-schema/test/models.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from "assert";
import assert, { deepStrictEqual } from "assert";
import { emitSchema } from "./utils.js";

describe("emitting models", () => {
Expand Down Expand Up @@ -254,4 +254,73 @@ describe("emitting models", () => {
$ref: "RecordInt32.json",
});
});

describe("default values", () => {
it("specify default value on enum property", async () => {
const res = await emitSchema(
`
model Foo {
optionalEnum?: MyEnum = MyEnum.a;
};

enum MyEnum {
a: "a-value",
b,
}
`
);

deepStrictEqual(res["Foo.json"].properties.optionalEnum, {
$ref: "MyEnum.json",
default: "a-value",
});
});

it("specify default value on string property", async () => {
timotheeguerin marked this conversation as resolved.
Show resolved Hide resolved
const res = await emitSchema(
`
model Foo {
optional?: string = "abc";
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "string",
default: "abc",
});
});

it("specify default value on numeric property", async () => {
const res = await emitSchema(
`
model Foo {
optional?: int32 = 123;
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "integer",
minimum: -2147483648,
maximum: 2147483647,
default: 123,
});
});

it("specify default value on boolean property", async () => {
const res = await emitSchema(
`
model Foo {
optional?: boolean = true;
}
`
);

deepStrictEqual(res["Foo.json"].properties.optional, {
type: "boolean",
default: true,
});
});
});
});