Skip to content

Json Schema: Add support for simple defaults #2657

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

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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 () => {
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,
});
});
});
});