diff --git a/.chronus/changes/fix-http-server-js-enum-json-transpose-2026-3-17-3-45-3.md b/.chronus/changes/fix-http-server-js-enum-json-transpose-2026-3-17-3-45-3.md new file mode 100644 index 00000000000..93a8c59662b --- /dev/null +++ b/.chronus/changes/fix-http-server-js-enum-json-transpose-2026-3-17-3-45-3.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-js" +--- + +fix: handle Enum type in JSON serialization transpose helpers to prevent crash when a model with an enum property requires a JSON serializer diff --git a/packages/http-server-js/src/common/serialization/json.ts b/packages/http-server-js/src/common/serialization/json.ts index 51f1d852012..d8a7862c29a 100644 --- a/packages/http-server-js/src/common/serialization/json.ts +++ b/packages/http-server-js/src/common/serialization/json.ts @@ -294,6 +294,8 @@ export function transposeExpressionToJson( } case "ModelProperty": return transposeExpressionToJson(ctx, type.type, expr, module); + case "Enum": + return expr; case "Intrinsic": switch (type.name) { case "void": @@ -537,6 +539,8 @@ export function transposeExpressionFromJson( } case "ModelProperty": return transposeExpressionFromJson(ctx, type.type, expr, module); + case "Enum": + return expr; case "Intrinsic": switch (type.name) { case "ErrorType": @@ -558,7 +562,6 @@ export function transposeExpressionFromJson( case "Boolean": return literalToExpr(type); case "Interface": - case "Enum": case "EnumMember": case "TemplateParameter": case "Namespace": diff --git a/packages/http-server-js/test/json-serialization.test.ts b/packages/http-server-js/test/json-serialization.test.ts new file mode 100644 index 00000000000..a27567129da --- /dev/null +++ b/packages/http-server-js/test/json-serialization.test.ts @@ -0,0 +1,118 @@ +import { Model, ModelProperty } from "@typespec/compiler"; +import { BasicTestRunner, createTestRunner } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { + emitJsonSerialization, + requiresJsonSerialization, +} from "../src/common/serialization/json.js"; +import { createInitialContext } from "../src/ctx.js"; +import { JsEmitterOptions } from "../src/lib.js"; +import { objectLiteralProperty, parseCase } from "../src/util/case.js"; +import { keywordSafe } from "../src/util/keywords.js"; + +describe("json serialization", () => { + let runner: BasicTestRunner; + + const defaultOptions: JsEmitterOptions = { + express: false, + "no-format": false, + "omit-unreachable-types": false, + }; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + async function getModels() { + const compiled = (await runner.compile(` + @service(#{ title: "Test" }) + namespace Test; + + enum MyEnum { + A, + B, + } + + model KeywordModel { + type: MyEnum; + } + + model RenamedModel { + wire_type: MyEnum; + } + + model TestModels { + @test keyword: KeywordModel; + @test renamed: RenamedModel; + } + `)) as { + keyword: ModelProperty; + renamed: ModelProperty; + }; + + if (compiled.keyword.type.kind !== "Model" || compiled.renamed.type.kind !== "Model") { + throw new Error("Expected @test properties to reference models."); + } + + const ctx = await createInitialContext(runner.program, defaultOptions); + + if (!ctx) { + throw new Error("Expected emitter context."); + } + + return { + ctx, + keywordModel: compiled.keyword.type, + renamedModel: compiled.renamed.type, + }; + } + + function emitModelSerialization( + ctx: NonNullable>>, + model: Model, + ) { + const module = ctx.globalNamespaceModule; + return [...emitJsonSerialization(ctx, model, module, model.name)]; + } + + function getGeneratedPropertyName(name: string): string { + return keywordSafe(parseCase(name).camelCase); + } + + it("serializes enum properties when a keyword-safe property name forces model serialization", async () => { + const { ctx, keywordModel } = await getModels(); + + strictEqual(requiresJsonSerialization(ctx, ctx.globalNamespaceModule, keywordModel), true); + + const lines = emitModelSerialization(ctx, keywordModel); + + ok( + lines.includes( + ` ${objectLiteralProperty("type")}: input.${getGeneratedPropertyName("type")},`, + ), + ); + ok(lines.includes(` ${getGeneratedPropertyName("type")}: input.type,`)); + }); + + it("serializes enum properties when a renamed property forces model serialization", async () => { + const { ctx, renamedModel } = await getModels(); + + strictEqual(requiresJsonSerialization(ctx, ctx.globalNamespaceModule, renamedModel), true); + + const lines = emitModelSerialization(ctx, renamedModel); + + deepStrictEqual(lines, [ + "toJsonObject(input: RenamedModel): any {", + " return {", + ` ${objectLiteralProperty("wire_type")}: input.${getGeneratedPropertyName("wire_type")},`, + " };", + "},", + "fromJsonObject(input: any): RenamedModel {", + " return {", + ` ${getGeneratedPropertyName("wire_type")}: input.wire_type,`, + " };", + "},", + ]); + }); +});