diff --git a/.chronus/changes/encode-numeric-as-string-2024-6-25-20-9-18.md b/.chronus/changes/encode-numeric-as-string-2024-6-25-20-9-18.md
new file mode 100644
index 0000000000..8ecec9e7c0
--- /dev/null
+++ b/.chronus/changes/encode-numeric-as-string-2024-6-25-20-9-18.md
@@ -0,0 +1,10 @@
+---
+# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
+changeKind: feature
+packages:
+ - "@typespec/compiler"
+ - "@typespec/openapi3"
+ - "@typespec/xml"
+---
+
+Add support for encoding numeric types as string
diff --git a/docs/libraries/http/encoding.md b/docs/libraries/http/encoding.md
index e680334c62..7f665c4fe9 100644
--- a/docs/libraries/http/encoding.md
+++ b/docs/libraries/http/encoding.md
@@ -134,3 +134,41 @@ model User {
+
+## Numeric types ( `int64`, `decimal128`, `float64`, etc.)
+
+By default numeric types are serialized as a JSON number. However for large types like `int64` or `decimal128` that cannot be represented in certain languages like JavaScript it is recommended to serialize them as string over the wire.
+
+
+TypeSpec | Example payload |
+
+
+
+```tsp
+model User {
+ id: int64; // JSON number
+
+ @encode(string)
+ idAsString: int64; // JSON string
+
+ viaSalar: decimalString;
+}
+
+@encode(string)
+scalar decimalString extends decimal128;
+```
+
+ |
+
+
+```json
+{
+ "id": 1234567890123456789012345678901234567890,
+ "idAsString": "1234567890123456789012345678901234567890",
+ "viaSalar": "1.3"
+}
+```
+
+ |
+
+
diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md
index 7317eae744..d30994aa58 100644
--- a/docs/standard-library/built-in-decorators.md
+++ b/docs/standard-library/built-in-decorators.md
@@ -100,7 +100,7 @@ model Pet {}
Specify how to encode the target type.
```typespec
-@encode(encoding: string | EnumMember, encodedAs?: Scalar)
+@encode(encodingOrEncodeAs: Scalar | valueof string | EnumMember, encodedAs?: Scalar)
```
#### Target
@@ -110,7 +110,7 @@ Specify how to encode the target type.
#### Parameters
| Name | Type | Description |
|------|------|-------------|
-| encoding | `string \| EnumMember` | Known name of an encoding. |
+| encodingOrEncodeAs | `Scalar` \| `valueof string \| EnumMember` | Known name of an encoding or a scalar type to encode as(Only for numeric types to encode as string). |
| encodedAs | `Scalar` | What target type is this being encoded as. Default to string. |
#### Examples
@@ -130,6 +130,15 @@ scalar myDateTime extends offsetDateTime;
scalar myDateTime extends unixTimestamp;
```
+##### encode numeric type to string
+
+
+```tsp
+model Pet {
+ @encode(string) id: int64;
+}
+```
+
### `@encodedName` {#@encodedName}
diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts
index 8ff600c510..5d6514db23 100644
--- a/packages/compiler/generated-defs/TypeSpec.ts
+++ b/packages/compiler/generated-defs/TypeSpec.ts
@@ -1,6 +1,7 @@
import type {
DecoratorContext,
Enum,
+ EnumValue,
Interface,
Model,
ModelProperty,
@@ -26,7 +27,7 @@ export interface OperationExample {
/**
* Specify how to encode the target type.
*
- * @param encoding Known name of an encoding.
+ * @param encodingOrEncodeAs Known name of an encoding or a scalar type to encode as(Only for numeric types to encode as string).
* @param encodedAs What target type is this being encoded as. Default to string.
* @example offsetDateTime encoded with rfc7231
*
@@ -40,11 +41,18 @@ export interface OperationExample {
* @encode("unixTimestamp", int32)
* scalar myDateTime extends unixTimestamp;
* ```
+ * @example encode numeric type to string
+ *
+ * ```tsp
+ * model Pet {
+ * @encode(string) id: int64;
+ * }
+ * ```
*/
export type EncodeDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
- encoding: Type,
+ encodingOrEncodeAs: Scalar | string | EnumValue,
encodedAs?: Scalar
) => void;
diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp
index 48e685b779..efd604dcd2 100644
--- a/packages/compiler/lib/std/decorators.tsp
+++ b/packages/compiler/lib/std/decorators.tsp
@@ -477,7 +477,7 @@ enum BytesKnownEncoding {
/**
* Specify how to encode the target type.
- * @param encoding Known name of an encoding.
+ * @param encodingOrEncodeAs Known name of an encoding or a scalar type to encode as(Only for numeric types to encode as string).
* @param encodedAs What target type is this being encoded as. Default to string.
*
* @example offsetDateTime encoded with rfc7231
@@ -493,10 +493,18 @@ enum BytesKnownEncoding {
* @encode("unixTimestamp", int32)
* scalar myDateTime extends unixTimestamp;
* ```
+ *
+ * @example encode numeric type to string
+ *
+ * ```tsp
+ * model Pet {
+ * @encode(string) id: int64;
+ * }
+ * ```
*/
extern dec encode(
target: Scalar | ModelProperty,
- encoding: string | EnumMember,
+ encodingOrEncodeAs: (valueof string | EnumMember) | Scalar,
encodedAs?: Scalar
);
diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts
index 025d50a5ef..b4e832f950 100644
--- a/packages/compiler/src/core/messages.ts
+++ b/packages/compiler/src/core/messages.ts
@@ -854,6 +854,7 @@ const diagnostics = {
wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type '${"type"}'. Expected: ${"expected"}.`,
wrongEncodingType: paramMessage`Encoding '${"encoding"}' on type '${"type"}' is expected to be serialized as '${"expected"}' but got '${"actual"}'.`,
wrongNumericEncodingType: paramMessage`Encoding '${"encoding"}' on type '${"type"}' is expected to be serialized as '${"expected"}' but got '${"actual"}'. Set '@encode' 2nd parameter to be of type ${"expected"}. e.g. '@encode("${"encoding"}", int32)'`,
+ firstArg: `First argument of "@encode" must be the encoding name or the string type when encoding numeric types.`,
},
},
diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts
index 0535acbc87..90961eca1c 100644
--- a/packages/compiler/src/lib/decorators.ts
+++ b/packages/compiler/src/lib/decorators.ts
@@ -55,6 +55,7 @@ import {
getTypeName,
ignoreDiagnostics,
isArrayModelType,
+ isValue,
reportDeprecated,
validateDecoratorUniqueOnNode,
} from "../core/index.js";
@@ -90,6 +91,7 @@ import {
DiagnosticTarget,
Enum,
EnumMember,
+ EnumValue,
Interface,
Model,
ModelProperty,
@@ -682,8 +684,13 @@ export function isSecret(program: Program, target: Type): boolean | undefined {
export type DateTimeKnownEncoding = "rfc3339" | "rfc7231" | "unixTimestamp";
export type DurationKnownEncoding = "ISO8601" | "seconds";
export type BytesKnownEncoding = "base64" | "base64url";
+
export interface EncodeData {
- encoding: DateTimeKnownEncoding | DurationKnownEncoding | BytesKnownEncoding | string;
+ /**
+ * Known encoding key.
+ * Can be undefined when `@encode(string)` is used on a numeric type. In that case it just means using the base10 decimal representation of the number.
+ */
+ encoding?: DateTimeKnownEncoding | DurationKnownEncoding | BytesKnownEncoding | string;
type: Scalar;
}
@@ -691,38 +698,48 @@ const encodeKey = createStateSymbol("encode");
export const $encode: EncodeDecorator = (
context: DecoratorContext,
target: Scalar | ModelProperty,
- encoding: string | Type,
+ encoding: string | EnumValue | Scalar,
encodeAs?: Scalar
) => {
validateDecoratorUniqueOnNode(context, target, $encode);
- const encodingStr = computeEncoding(encoding);
- if (encodingStr === undefined) {
+ const encodeData = computeEncoding(context.program, encoding, encodeAs);
+ if (encodeData === undefined) {
return;
}
- const encodeData: EncodeData = {
- encoding: encodingStr,
- type: encodeAs ?? context.program.checker.getStdType("string"),
- };
const targetType = getPropertyType(target);
validateEncodeData(context, targetType, encodeData);
context.program.stateMap(encodeKey).set(target, encodeData);
};
-function computeEncoding(encoding: string | Type) {
- if (typeof encoding === "string") {
- return encoding;
- }
- switch (encoding.kind) {
- case "String":
- return encoding.value;
- case "EnumMember":
- if (encoding.value && typeof encoding.value === "string") {
- return encoding.value;
- } else {
- return getTypeName(encoding);
- }
- default:
+
+function computeEncoding(
+ program: Program,
+ encodingOrEncodeAs: string | EnumValue | Scalar,
+ encodeAs: Scalar | undefined
+): EncodeData | undefined {
+ const strType = program.checker.getStdType("string");
+ const resolvedEncodeAs = encodeAs ?? strType;
+ if (typeof encodingOrEncodeAs === "string") {
+ return { encoding: encodingOrEncodeAs, type: resolvedEncodeAs };
+ } else if (isValue(encodingOrEncodeAs)) {
+ const member = encodingOrEncodeAs.value;
+ if (member.value && typeof member.value === "string") {
+ return { encoding: member.value, type: resolvedEncodeAs };
+ } else {
+ return { encoding: getTypeName(member), type: resolvedEncodeAs };
+ }
+ } else {
+ const originalType = encodingOrEncodeAs.projectionBase ?? encodingOrEncodeAs;
+ if (originalType !== strType) {
+ reportDiagnostic(program, {
+ code: "invalid-encode",
+ messageId: "firstArg",
+ target: encodingOrEncodeAs,
+ });
return undefined;
+ }
+
+ return { type: encodingOrEncodeAs };
}
}
@@ -742,7 +759,7 @@ function validateEncodeData(context: DecoratorContext, target: Type, encodeData:
code: "invalid-encode",
messageId: "wrongType",
format: {
- encoding: encodeData.encoding,
+ encoding: encodeData.encoding ?? "string",
type: getTypeName(target),
expected: validTargets.join(", "),
},
@@ -763,11 +780,11 @@ function validateEncodeData(context: DecoratorContext, target: Type, encodeData:
const typeName = getTypeName(encodeData.type.projectionBase ?? encodeData.type);
reportDiagnostic(context.program, {
code: "invalid-encode",
- messageId: ["unixTimestamp", "seconds"].includes(encodeData.encoding)
+ messageId: ["unixTimestamp", "seconds"].includes(encodeData.encoding ?? "string")
? "wrongNumericEncodingType"
: "wrongEncodingType",
format: {
- encoding: encodeData.encoding,
+ encoding: encodeData.encoding!,
type: getTypeName(target),
expected: validEncodeTypes.join(", "),
actual: typeName,
@@ -790,6 +807,8 @@ function validateEncodeData(context: DecoratorContext, target: Type, encodeData:
return check(["bytes"], ["string"]);
case "base64url":
return check(["bytes"], ["string"]);
+ case undefined:
+ return check(["numeric"], ["string"]);
}
}
diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts
index c8237af518..ebe846addd 100644
--- a/packages/compiler/test/decorators/decorators.test.ts
+++ b/packages/compiler/test/decorators/decorators.test.ts
@@ -731,6 +731,19 @@ describe("compiler: built-in decorators", () => {
strictEqual(encodeData.type.name, encodeAs ?? "string");
});
});
+
+ it(`@encode(string) on numeric scalar`, async () => {
+ const { s } = (await runner.compile(`
+ @encode(string)
+ @test
+ scalar s extends int64;
+ `)) as { s: Scalar };
+
+ const encodeData = getEncode(runner.program, s);
+ ok(encodeData);
+ strictEqual(encodeData.encoding, undefined);
+ strictEqual(encodeData.type.name, "string");
+ });
});
describe("invalid", () => {
invalidCases.forEach(([target, encoding, encodeAs, expectedCode, expectedMessage]) => {
@@ -750,6 +763,20 @@ describe("compiler: built-in decorators", () => {
});
});
});
+
+ it(`@encode(string) on non-numeric scalar`, async () => {
+ const diagnostics = await runner.diagnose(`
+ @encode(string)
+ @test
+ scalar s extends utcDateTime;
+ `);
+
+ expectDiagnostics(diagnostics, {
+ code: "invalid-encode",
+ severity: "error",
+ message: "Encoding 'string' cannot be used on type 's'. Expected: numeric.",
+ });
+ });
});
});
});
diff --git a/packages/openapi3/src/encoding.ts b/packages/openapi3/src/encoding.ts
new file mode 100644
index 0000000000..efad0f0c28
--- /dev/null
+++ b/packages/openapi3/src/encoding.ts
@@ -0,0 +1,57 @@
+import { type ModelProperty, Program, type Scalar, getEncode } from "@typespec/compiler";
+import type { ResolvedOpenAPI3EmitterOptions } from "./openapi.js";
+import { getSchemaForStdScalars } from "./std-scalar-schemas.js";
+import type { OpenAPI3Schema } from "./types.js";
+
+export function applyEncoding(
+ program: Program,
+ typespecType: Scalar | ModelProperty,
+ target: OpenAPI3Schema,
+ options: ResolvedOpenAPI3EmitterOptions
+): OpenAPI3Schema {
+ const encodeData = getEncode(program, typespecType);
+ if (encodeData) {
+ const newTarget = { ...target };
+ const newType = getSchemaForStdScalars(encodeData.type as any, options);
+ newTarget.type = newType.type;
+ // If the target already has a format it takes priority. (e.g. int32)
+ newTarget.format = mergeFormatAndEncoding(
+ newTarget.format,
+ encodeData.encoding,
+ newType.format
+ );
+ return newTarget;
+ }
+ return target;
+}
+
+function mergeFormatAndEncoding(
+ format: string | undefined,
+ encoding: string | undefined,
+ encodeAsFormat: string | undefined
+): string | undefined {
+ switch (format) {
+ case undefined:
+ return encodeAsFormat ?? encoding ?? format;
+ case "date-time":
+ switch (encoding) {
+ case "rfc3339":
+ return "date-time";
+ case "unixTimestamp":
+ return "unixtime";
+ case "rfc7231":
+ return "http-date";
+ default:
+ return encoding;
+ }
+ case "duration":
+ switch (encoding) {
+ case "ISO8601":
+ return "duration";
+ default:
+ return encodeAsFormat ?? encoding;
+ }
+ default:
+ return encodeAsFormat ?? encoding ?? format;
+ }
+}
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 4a0041a075..073e469567 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -10,7 +10,6 @@ import {
getAllTags,
getAnyExtensionFromPath,
getDoc,
- getEncode,
getFormat,
getKnownValues,
getMaxItems,
@@ -44,7 +43,6 @@ import {
ProjectionApplication,
projectProgram,
resolvePath,
- Scalar,
serializeValueAsJson,
Service,
Type,
@@ -97,6 +95,7 @@ import {
import { buildVersionProjections, VersionProjections } from "@typespec/versioning";
import { stringify } from "yaml";
import { getRef } from "./decorators.js";
+import { applyEncoding } from "./encoding.js";
import { createDiagnostic, FileType, OpenAPI3EmitterOptions } from "./lib.js";
import { getDefaultValue, isBytesKeptRaw, OpenAPI3SchemaEmitter } from "./schema-emitter.js";
import {
@@ -1476,7 +1475,12 @@ function createOAPIEmitter(
if (!typeSchema) {
return undefined;
}
- const schema = applyEncoding(param, applyIntrinsicDecorators(param, typeSchema));
+ const schema = applyEncoding(
+ program,
+ param,
+ applyIntrinsicDecorators(param, typeSchema),
+ options
+ );
if (param.defaultValue) {
schema.default = getDefaultValue(program, param.defaultValue);
}
@@ -1746,61 +1750,6 @@ function createOAPIEmitter(
return newTarget;
}
- function applyEncoding(
- typespecType: Scalar | ModelProperty,
- target: OpenAPI3Schema
- ): OpenAPI3Schema {
- const encodeData = getEncode(program, typespecType);
- if (encodeData) {
- const newTarget = { ...target };
- const newType = callSchemaEmitter(
- encodeData.type,
- Visibility.Read,
- false,
- "application/json"
- ) as OpenAPI3Schema;
- newTarget.type = newType.type;
- // If the target already has a format it takes priority. (e.g. int32)
- newTarget.format = mergeFormatAndEncoding(
- newTarget.format,
- encodeData.encoding,
- newType.format
- );
- return newTarget;
- }
- return target;
- }
- function mergeFormatAndEncoding(
- format: string | undefined,
- encoding: string,
- encodeAsFormat: string | undefined
- ): string {
- switch (format) {
- case undefined:
- return encodeAsFormat ?? encoding;
- case "date-time":
- switch (encoding) {
- case "rfc3339":
- return "date-time";
- case "unixTimestamp":
- return "unixtime";
- case "rfc7231":
- return "http-date";
- default:
- return encoding;
- }
- case "duration":
- switch (encoding) {
- case "ISO8601":
- return "duration";
- default:
- return encodeAsFormat ?? encoding;
- }
- default:
- return encodeAsFormat ?? encoding;
- }
- }
-
function applyExternalDocs(typespecType: Type, target: Record) {
const externalDocs = getExternalDocs(program, typespecType);
if (externalDocs) {
diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts
index 00a6885dd4..04b276afbd 100644
--- a/packages/openapi3/src/schema-emitter.ts
+++ b/packages/openapi3/src/schema-emitter.ts
@@ -73,8 +73,10 @@ import {
shouldInline,
} from "@typespec/openapi";
import { getOneOf, getRef } from "./decorators.js";
+import { applyEncoding } from "./encoding.js";
import { OpenAPI3EmitterOptions, reportDiagnostic } from "./lib.js";
import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js";
+import { getSchemaForStdScalars } from "./std-scalar-schemas.js";
import { OpenAPI3Discriminator, OpenAPI3Schema, OpenAPI3SchemaProperty } from "./types.js";
import { VisibilityUsageTracker } from "./visibility-usage.js";
@@ -718,66 +720,7 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter<
}
#getSchemaForStdScalars(scalar: Scalar & { name: IntrinsicScalarName }): OpenAPI3Schema {
- switch (scalar.name) {
- case "bytes":
- return { type: "string", format: "byte" };
- case "numeric":
- return { type: "number" };
- case "integer":
- return { type: "integer" };
- case "int8":
- return { type: "integer", format: "int8" };
- case "int16":
- return { type: "integer", format: "int16" };
- case "int32":
- return { type: "integer", format: "int32" };
- case "int64":
- return { type: "integer", format: "int64" };
- case "safeint":
- switch (this.#options.safeintStrategy) {
- case "double-int":
- return { type: "integer", format: "double-int" };
- case "int64":
- default:
- return { type: "integer", format: "int64" };
- }
- case "uint8":
- return { type: "integer", format: "uint8" };
- case "uint16":
- return { type: "integer", format: "uint16" };
- case "uint32":
- return { type: "integer", format: "uint32" };
- case "uint64":
- return { type: "integer", format: "uint64" };
- case "float":
- return { type: "number" };
- case "float64":
- return { type: "number", format: "double" };
- case "float32":
- return { type: "number", format: "float" };
- case "decimal":
- return { type: "number", format: "decimal" };
- case "decimal128":
- return { type: "number", format: "decimal128" };
- case "string":
- return { type: "string" };
- case "boolean":
- return { type: "boolean" };
- case "plainDate":
- return { type: "string", format: "date" };
- case "utcDateTime":
- case "offsetDateTime":
- return { type: "string", format: "date-time" };
- case "plainTime":
- return { type: "string", format: "time" };
- case "duration":
- return { type: "string", format: "duration" };
- case "url":
- return { type: "string", format: "uri" };
- default:
- const _assertNever: never = scalar.name;
- return {};
- }
+ return getSchemaForStdScalars(scalar, this.#options);
}
#applySchemaExamples(
@@ -904,31 +847,16 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter<
typespecType: Scalar | ModelProperty,
target: OpenAPI3Schema | Placeholder
): OpenAPI3Schema {
- const encodeData = getEncode(this.emitter.getProgram(), typespecType);
- if (encodeData) {
- const newTarget = new ObjectBuilder(target);
- const newType = this.#getSchemaForStdScalars(encodeData.type as any);
-
- newTarget.type = newType.type;
- // If the target already has a format it takes priority. (e.g. int32)
- newTarget.format = this.#mergeFormatAndEncoding(
- newTarget.format,
- encodeData.encoding,
- newType.format
- );
- return newTarget;
- }
- const result = new ObjectBuilder(target);
- return result;
+ return applyEncoding(this.emitter.getProgram(), typespecType, target as any, this.#options);
}
#mergeFormatAndEncoding(
format: string | undefined,
- encoding: string,
+ encoding: string | undefined,
encodeAsFormat: string | undefined
- ): string {
+ ): string | undefined {
switch (format) {
case undefined:
- return encodeAsFormat ?? encoding;
+ return encodeAsFormat ?? encoding ?? format;
case "date-time":
switch (encoding) {
case "rfc3339":
diff --git a/packages/openapi3/src/std-scalar-schemas.ts b/packages/openapi3/src/std-scalar-schemas.ts
new file mode 100644
index 0000000000..96075ef6ac
--- /dev/null
+++ b/packages/openapi3/src/std-scalar-schemas.ts
@@ -0,0 +1,69 @@
+import type { IntrinsicScalarName, Scalar } from "@typespec/compiler";
+import type { ResolvedOpenAPI3EmitterOptions } from "./openapi.js";
+import type { OpenAPI3Schema } from "./types.js";
+
+export function getSchemaForStdScalars(
+ scalar: Scalar & { name: IntrinsicScalarName },
+ options: ResolvedOpenAPI3EmitterOptions
+): OpenAPI3Schema {
+ switch (scalar.name) {
+ case "bytes":
+ return { type: "string", format: "byte" };
+ case "numeric":
+ return { type: "number" };
+ case "integer":
+ return { type: "integer" };
+ case "int8":
+ return { type: "integer", format: "int8" };
+ case "int16":
+ return { type: "integer", format: "int16" };
+ case "int32":
+ return { type: "integer", format: "int32" };
+ case "int64":
+ return { type: "integer", format: "int64" };
+ case "safeint":
+ switch (options.safeintStrategy) {
+ case "double-int":
+ return { type: "integer", format: "double-int" };
+ case "int64":
+ default:
+ return { type: "integer", format: "int64" };
+ }
+ case "uint8":
+ return { type: "integer", format: "uint8" };
+ case "uint16":
+ return { type: "integer", format: "uint16" };
+ case "uint32":
+ return { type: "integer", format: "uint32" };
+ case "uint64":
+ return { type: "integer", format: "uint64" };
+ case "float":
+ return { type: "number" };
+ case "float64":
+ return { type: "number", format: "double" };
+ case "float32":
+ return { type: "number", format: "float" };
+ case "decimal":
+ return { type: "number", format: "decimal" };
+ case "decimal128":
+ return { type: "number", format: "decimal128" };
+ case "string":
+ return { type: "string" };
+ case "boolean":
+ return { type: "boolean" };
+ case "plainDate":
+ return { type: "string", format: "date" };
+ case "utcDateTime":
+ case "offsetDateTime":
+ return { type: "string", format: "date-time" };
+ case "plainTime":
+ return { type: "string", format: "time" };
+ case "duration":
+ return { type: "string", format: "duration" };
+ case "url":
+ return { type: "string", format: "uri" };
+ default:
+ const _assertNever: never = scalar.name;
+ return {};
+ }
+}
diff --git a/packages/openapi3/test/primitive-types.test.ts b/packages/openapi3/test/primitive-types.test.ts
index 2a430823c4..9ee9f8e4af 100644
--- a/packages/openapi3/test/primitive-types.test.ts
+++ b/packages/openapi3/test/primitive-types.test.ts
@@ -259,11 +259,16 @@ describe("openapi3: primitives", () => {
async function testEncode(
scalar: string,
expectedOpenApi: OpenAPI3Schema,
- encoding?: string,
+ encoding?: string | null,
encodeAs?: string
) {
const encodeAsParam = encodeAs ? `, ${encodeAs}` : "";
- const encodeDecorator = encoding ? `@encode("${encoding}"${encodeAsParam})` : "";
+ const encodeDecorator =
+ encoding === null
+ ? `@encode(${encodeAs})`
+ : encoding !== undefined
+ ? `@encode("${encoding}"${encodeAsParam})`
+ : "";
const res1 = await oapiForModel("s", `${encodeDecorator} scalar s extends ${scalar};`);
deepStrictEqual(res1.schemas.s, expectedOpenApi);
const res2 = await oapiForModel("Test", `model Test {${encodeDecorator} prop: ${scalar}};`);
@@ -309,5 +314,19 @@ describe("openapi3: primitives", () => {
it("set format to base64url when encoding bytes as base64url", () =>
testEncode("bytes", { type: "string", format: "base64url" }, "base64url"));
});
+
+ describe("int64", () => {
+ it("set type: integer and format to 'int64' by default", () =>
+ testEncode("int64", { type: "integer", format: "int64" }));
+ it("set type: string and format to int64 when @encode(string)", () =>
+ testEncode("int64", { type: "string", format: "int64" }, null, "string"));
+ });
+
+ describe("decimal128", () => {
+ it("set type: integer and format to 'int64' by default", () =>
+ testEncode("decimal128", { type: "number", format: "decimal128" }));
+ it("set type: string and format to int64 when @encode(string)", () =>
+ testEncode("decimal128", { type: "string", format: "decimal128" }, null, "string"));
+ });
});
});
diff --git a/packages/xml/src/types.ts b/packages/xml/src/types.ts
index 9fabad38ca..93e027a248 100644
--- a/packages/xml/src/types.ts
+++ b/packages/xml/src/types.ts
@@ -21,6 +21,6 @@ export type XmlEncoding =
| "TypeSpec.Xml.Encoding.xmlBase64Binary";
export interface XmlEncodeData extends EncodeData {
- encoding: XmlEncoding | EncodeData["encoding"];
+ encoding?: XmlEncoding | EncodeData["encoding"];
type: Scalar;
}