Skip to content

Implement array encoding for model properties#3659

Merged
JialinHuang803 merged 16 commits intoAzure:mainfrom
JialinHuang803:array-encoding
Jan 4, 2026
Merged

Implement array encoding for model properties#3659
JialinHuang803 merged 16 commits intoAzure:mainfrom
JialinHuang803:array-encoding

Conversation

@JialinHuang803
Copy link
Copy Markdown
Member

@JialinHuang803 JialinHuang803 commented Dec 23, 2025

Resolve: #3604

Overview

This PR implements support for array encoding decorators @encode(ArrayEncoding.*) within TypeSpec model properties. Previously, array encoding was only supported for query and header parameters, but now it extends to model properties as well, enabling proper serialization and deserialization of array fields with different delimited formats. Currently only array of string type is supported for encoding.

What's included

Support 4 delimiters for encoding model properties of string array type, including ',', ' ', '|', and '\n'.

  • Added parsing helpers for CSV, SSV, pipe, and newline formats, and added serialization helper for newline format.
  • Updated the logic for serializing and deserializing array parameters and properties.
  • Refactored internal helpers for handling collection formats.
  • Introduced a new diagnostic warning (un-supported-array-encoding) that triggers when an array of an unsupported type is marked for encoding.

For example:

model Widget {
    @encode(ArrayEncoding.commaDelimited)
    requiredColors: string[];

    @encode(ArrayEncoding.spaceDelimited)
    optionalColors?: string[];
}

Serialization
string[] -> string, an empty array will be serialized as "".

export function widgetSerializer(item: Widget): any {
  return {
    requiredColors: buildCsvCollection(
      item["requiredColors"].map((p: any) => {
        return p;
      }),
    ),
    optionalColors: !item["optionalColors"]
      ? item["optionalColors"]
      : buildSsvCollection(
          item["optionalColors"].map((p: any) => {
            return p;
          }),
        ),
  };
}

Deserialization
string -> string[], an empty string "" will be deserialized as an empty array.

export function widgetDeserializer(item: any): Widget {
  return {
    requiredColors: parseCsvCollection(item["requiredColors"]),
    optionalColors:
      item["optionalColors"] === null || item["optionalColors"] === undefined
        ? item["optionalColors"]
        : parseSsvCollection(item["optionalColors"]),
  };
}

Impact on existing generated code

The generated code related to the collection format query and header parameter got changed if the parameter is optional, but its behavior should remain unchanged.
For example,
TypeSpec:

@get read(
  @path
  id: Widget.id,

  @query
  @encode(ArrayEncoding.pipeDelimited)
  colors?: string[],

  @header
  weights?: string[],
): Widget;

Before:

onst path = expandUrlTemplate(
  "/widgets/{id}{?colors}",
  {
    id: id,
    colors:
      options?.colors !== undefined
        ? buildPipeCollection(
            !options?.colors
              ? options?.colors
              : options?.colors.map((p: any) => {
                  return p;
                }),
          )
        : undefined,
  },
  {
    allowReserved: options?.requestOptions?.skipUrlEncoding,
  },
);
return context.path(path).get({
  ...operationOptionsToRequestParameters(options),
  headers: {
    ...(options?.weights !== undefined
      ? {
          weights:
            options?.weights !== undefined
              ? buildCsvCollection(
                  !options?.weights
                    ? options?.weights
                    : options?.weights.map((p: any) => {
                        return p;
                      }),
                )
              : undefined,
        }
      : {}),
    accept: "application/json",
    ...options.requestOptions?.headers,
  },
});

After:

const path = expandUrlTemplate(
  "/widgets/{id}{?colors}",
  {
    id: id,
    colors: !options?.colors
      ? options?.colors
      : buildPipeCollection(
          options?.colors.map((p: any) => {
            return p;
          }),
        ),
  },
  {
    allowReserved: options?.requestOptions?.skipUrlEncoding,
  },
);
return context.path(path).get({
  ...operationOptionsToRequestParameters(options),
  headers: {
    ...(options?.weights !== undefined
      ? {
          weights: !options?.weights
            ? options?.weights
            : buildCsvCollection(
                options?.weights.map((p: any) => {
                  return p;
                }),
              ),
        }
      : {}),
    accept: "application/json",
    ...options.requestOptions?.headers,
  },
});

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements comprehensive support for array encoding decorators (@encode(ArrayEncoding.*)) for TypeSpec model properties, extending functionality that previously existed only for query and header parameters. The implementation adds support for four delimiter formats: comma-delimited (CSV), space-delimited (SSV), pipe-delimited, and newline-delimited for string array properties.

Key Changes:

  • Added parsing helpers for CSV, SSV, pipe, and newline-delimited formats to convert encoded strings back to arrays
  • Added a build helper for newline-delimited format to convert arrays to encoded strings
  • Refactored collection format handling logic to support model properties in addition to parameters
  • Introduced a new diagnostic warning for unsupported array encodings (non-string array types)

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/typespec-ts/static/static-helpers/serialization/parse-csv-collection.ts New helper to parse comma-delimited strings into string arrays
packages/typespec-ts/static/static-helpers/serialization/parse-ssv-collection.ts New helper to parse space-delimited strings into string arrays
packages/typespec-ts/static/static-helpers/serialization/parse-pipe-collection.ts New helper to parse pipe-delimited strings into string arrays
packages/typespec-ts/static/static-helpers/serialization/parse-newline-collection.ts New helper to parse newline-delimited strings into string arrays
packages/typespec-ts/static/static-helpers/serialization/build-newline-collection.ts New helper to build newline-delimited strings from arrays
packages/typespec-ts/src/utils/operationUtil.ts Refactored collection format handling to support properties; added enum for known collection formats and helper functions
packages/typespec-ts/src/modular/static-helpers-metadata.ts Registered new parse and build helpers for collection formats
packages/typespec-ts/src/modular/helpers/operationHelpers.ts Updated serialization and deserialization logic to handle array encoding for model properties
packages/typespec-ts/src/lib.ts Added new diagnostic warning for unsupported array encoding types
packages/typespec-ts/test/modularUnit/scenarios/models/serialization/modelPropertyArrayEncoding.md New test scenarios demonstrating array encoding for model properties
packages/typespec-ts/test/modularUnit/scenarios/models/deserialization/propertyType.md Fixed typo in property name from "propStringUnionOptioanl" to "propStringUnionOptional"
packages/typespec-ts/test/modularUnit/scenarios/models/propertyFlatten/nameCollision.md Formatting updates adding trailing commas for consistency

Comment thread packages/typespec-ts/src/modular/helpers/operationHelpers.ts Outdated
Comment thread packages/typespec-ts/src/modular/helpers/operationHelpers.ts Outdated
property: SdkModelPropertyType
): string | undefined {
if (property.encode && property.type.kind === "array") {
// Only arrays of string type can have collectionFormat encoding
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do we get the agreement on only supporting string?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the TypeSpec issue mentioned maybe only string[] is supported. I think we can support only string[] for now and consider adding other primitive types if requested by other service teams.

default: paramMessage`Model name conflict detected: "${"modelName"}" exists in multiple namespaces: ${"namespaces"}. Please use @clientName to rename them.`
}
},
"un-supported-array-encoding": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have an UT for this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UT added.

return `${nullOrUndefinedPrefix}Object.fromEntries(Object.entries(${restValue}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}p]))`;
} else if (type.valueType) {
return `Object.fromEntries(Object.entries(${prefix}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", getEncodeForType(type.valueType))}]))`;
return `${nullOrUndefinedPrefix}Object.fromEntries(Object.entries(${restValue}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", true, getEncodeForType(type.valueType))}]))`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the requred value is fixed as true here? not devired from other values like prop.optional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to keep the behavior consistent with before. In addition, whether the value p should have optional prefix is decided by elementNullOrUndefinedPrefix.

: "";
return `${optionalPrefixForString}${parseHelper}(${restValue})`;
} else {
throw `No supported collection format parse helper found for ${format}.`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw exeptions? does that mean the emitter will crash?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception is added here because the original getCollectionFormat method throws an exception if getCollectionFormatHelper doesn't return a helper:

const collectionInfo = getCollectionFormatHelper(param.kind, format ?? "");
  if (!collectionInfo) {
    throw "Has collection format info but without helper function detected";
  }

I remove the logic from that method here. It's fine if we don't want to throw exceptions here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember we have some earlier discussion that emitters should not throw error (crash) on purpose instead we should reportDiagnostic #3064

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the exception.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, the code diff doesn't show as removed. Also, is possible to report diagnostic warning?

requiredCsvColors: parseCsvCollection(item["requiredCsvColors"]),
requiredPipeColors: parsePipeCollection(item["requiredPipeColors"]),
optionalSsvColors:
item["optionalSsvColors"] == null
Copy link
Copy Markdown
Member

@MaryGao MaryGao Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ususally we don't recommand to use "==" check, instead use "===" or more explicit expression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will update it to item["optionalSsvColors"] === null || item["optionalSsvColors"] === undefined.

@@ -0,0 +1,3 @@
export function parseCsvCollection(value: string): string[] {
return value ? value.split(",") : [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for the ? check since this is required?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value ? here is to ensure [] is returned if the input value is "". If return value.split(",") directly, an array containing an empty string [""] will be returned which is not expected.

): string | undefined {
if (property.encode && property.type.kind === "array") {
// Only arrays of string type can have collectionFormat encoding
if (property.type.valueType.kind !== "string") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if the string-like type is supported like ("a" | "b")[]?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question. I tested it, and it's not supported since it's an array of enum type...

case "commaDelimited":
return KnownCollectionFormat.Csv;
case "newlineDelimited":
return KnownCollectionFormat.Newline;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No KnownCollectionFormat.Multi for model property?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. ArrayEncoding only includes "pipeDelimited" | "spaceDelimited" | "commaDelimited" | "newlineDelimited".

@qiaozha qiaozha added the p0 priority 0 label Dec 24, 2025
Comment thread packages/typespec-ts/src/modular/helpers/operationHelpers.ts
: "";
return `${optionalPrefixForString}${parseHelper}(${restValue})`;
} else {
throw `No supported collection format parse helper found for ${format}.`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember we have some earlier discussion that emitters should not throw error (crash) on purpose instead we should reportDiagnostic #3064

Comment thread packages/typespec-ts/src/utils/operationUtil.ts
Copy link
Copy Markdown
Member

@qiaozha qiaozha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a minor comment left, feel free to merge if it's resolved.

: "";
return `${optionalPrefixForString}${parseHelper}(${restValue})`;
} else {
throw `No supported collection format parse helper found for ${format}.`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, the code diff doesn't show as removed. Also, is possible to report diagnostic warning?

@JialinHuang803 JialinHuang803 enabled auto-merge (squash) January 4, 2026 01:09
@JialinHuang803 JialinHuang803 merged commit 09452f5 into Azure:main Jan 4, 2026
16 checks passed
@JialinHuang803 JialinHuang803 deleted the array-encoding branch January 4, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p0 priority 0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[http-client-ts] CSV encoding for model properties Implementation

4 participants