Skip to content

Commit 446cb00

Browse files
joherediCopilot
andauthored
Fix XML object serializer missing itemsName wrapping for arrays (#3774)
The buildXmlObjectPropertyAssignments function was generating flat mappings for array properties without wrapping items under their itemsName element. This caused incorrect XML where array items were placed directly under the wrapper element, missing the intermediate item element (e.g., <Field>). For wrapped arrays, now generates: { "WrapperName": { "ItemsName": items } } For unwrapped arrays, now generates: { "ItemsName": items } Fixes #3771 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d805868 commit 446cb00

2 files changed

Lines changed: 215 additions & 1 deletion

File tree

packages/typespec-ts/src/modular/serialization/buildXmlSerializerFunction.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,34 @@ function buildXmlObjectPropertyAssignments(
313313
property.type.valueType.kind === "model"
314314
) {
315315
// Array of objects - map each item through XML object serializer
316-
valueExpr = `item["${cleanPropertyName}"]?.map((i: any) => ${nestedSerializer}(i))`;
316+
const mappedExpr = `item["${cleanPropertyName}"]?.map((i: any) => ${nestedSerializer}(i))`;
317+
if (xmlOptions?.unwrapped) {
318+
// Unwrapped: items are direct siblings under the item element name
319+
const itemKey = xmlOptions?.itemsName ?? xmlName;
320+
assignments.push(`"${itemKey}": ${mappedExpr}`);
321+
continue;
322+
} else if (xmlOptions?.itemsName) {
323+
// Wrapped: items nested under wrapper element with item element name
324+
valueExpr = `{ "${xmlOptions.itemsName}": ${mappedExpr} }`;
325+
} else {
326+
valueExpr = mappedExpr;
327+
}
328+
} else if (property.type.kind === "array") {
329+
// Array of primitives - handle itemsName wrapping
330+
const primitiveExpr = buildXmlValueSerializationExpr(
331+
context,
332+
property.type,
333+
`item["${cleanPropertyName}"]`
334+
);
335+
if (xmlOptions?.unwrapped) {
336+
const itemKey = xmlOptions?.itemsName ?? xmlName;
337+
assignments.push(`"${itemKey}": ${primitiveExpr}`);
338+
continue;
339+
} else if (xmlOptions?.itemsName) {
340+
valueExpr = `{ "${xmlOptions.itemsName}": ${primitiveExpr} }`;
341+
} else {
342+
valueExpr = primitiveExpr;
343+
}
317344
} else {
318345
// Handle type-specific serialization
319346
valueExpr = buildXmlValueSerializationExpr(
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# XML Wrapped Array with Model Items and @Xml.name
2+
3+
Tests that the XML object serializer correctly wraps array items with the item element name
4+
when the array item model has an `@Xml.name` decorator. This validates the fix for issue #3771
5+
where the `<Field>` wrapper element was missing in serialized XML for nested models with arrays.
6+
7+
## TypeSpec
8+
9+
```tsp
10+
/** Represents an Apache Arrow field. */
11+
@Xml.name("Field")
12+
model ArrowField {
13+
/** The arrow field type. */
14+
@Xml.name("Type") type: string;
15+
16+
/** The arrow field name. */
17+
@Xml.name("Name") name?: string;
18+
19+
/** The arrow field precision. */
20+
@Xml.name("Precision") precision?: int32;
21+
}
22+
23+
/** Represents the Apache Arrow configuration. */
24+
model ArrowConfiguration {
25+
/** The Apache Arrow schema */
26+
@Xml.name("Schema") schema: ArrowField[];
27+
}
28+
29+
@route("/arrow")
30+
interface ArrowOperations {
31+
/** Set arrow configuration */
32+
@put setConfig(@body body: ArrowConfiguration): void;
33+
34+
/** Get arrow configuration */
35+
@get getConfig(): ArrowConfiguration;
36+
}
37+
```
38+
39+
## Provide generated models with correct XML object serializers
40+
41+
The key validation is that `arrowConfigurationXmlObjectSerializer` wraps array items
42+
under the `"Field"` item element name within the `"Schema"` wrapper element, producing
43+
the structure `{ "Schema": { "Field": [items...] } }` instead of `{ "Schema": [items...] }`.
44+
45+
```ts models interface ArrowField
46+
/** Represents an Apache Arrow field. */
47+
export interface ArrowField {
48+
/** The arrow field type. */
49+
type: string;
50+
/** The arrow field name. */
51+
name?: string;
52+
/** The arrow field precision. */
53+
precision?: number;
54+
}
55+
```
56+
57+
```ts models interface ArrowConfiguration
58+
/**
59+
* This file contains only generated model types and their (de)serializers.
60+
* Disable the following rules for internal models with '_' prefix and deserializers which require 'any' for raw JSON input.
61+
*/
62+
/* eslint-disable @typescript-eslint/naming-convention */
63+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
64+
/** Represents the Apache Arrow configuration. */
65+
export interface ArrowConfiguration {
66+
/** The Apache Arrow schema */
67+
schema: ArrowField[];
68+
}
69+
```
70+
71+
```ts models function arrowFieldXmlObjectSerializer
72+
export function arrowFieldXmlObjectSerializer(item: ArrowField): XmlSerializedObject {
73+
return { Type: item["type"], Name: item["name"], Precision: item["precision"] };
74+
}
75+
```
76+
77+
```ts models function arrowConfigurationXmlSerializer
78+
export function arrowConfigurationXmlSerializer(item: ArrowConfiguration): string {
79+
const properties: XmlPropertyMetadata[] = [
80+
{
81+
propertyName: "schema",
82+
xmlOptions: { name: "Schema", itemsName: "Field" },
83+
type: "array",
84+
serializer: arrowFieldXmlObjectSerializer,
85+
},
86+
];
87+
return serializeToXml(item, properties, "ArrowConfiguration");
88+
}
89+
```
90+
91+
```ts models function arrowConfigurationXmlObjectSerializer
92+
export function arrowConfigurationXmlObjectSerializer(
93+
item: ArrowConfiguration,
94+
): XmlSerializedObject {
95+
return { Schema: { Field: item["schema"]?.map((i: any) => arrowFieldXmlObjectSerializer(i)) } };
96+
}
97+
```
98+
99+
# XML Unwrapped Array with Model Items Serialization
100+
101+
Tests that the XML object serializer correctly handles unwrapped arrays of model items
102+
in the serialization direction. Unwrapped items should appear as direct siblings using the item name.
103+
104+
## TypeSpec
105+
106+
```tsp
107+
/** A single tag */
108+
model BlobTag {
109+
/** Tag key */
110+
@Xml.name("Key")
111+
key: string;
112+
113+
/** Tag value */
114+
@Xml.name("Value")
115+
value: string;
116+
}
117+
118+
/** Container for tags */
119+
@Xml.name("Tags")
120+
model BlobTags {
121+
/** The tag set */
122+
@Xml.name("TagSet")
123+
@Xml.unwrapped
124+
blobTagSet: BlobTag[];
125+
}
126+
127+
@route("/tags")
128+
interface TagOperations {
129+
/** Set tags */
130+
@put setTags(@body body: BlobTags): void;
131+
}
132+
```
133+
134+
## Provide generated models with correct unwrapped array serializers
135+
136+
The key validation is that `blobTagsXmlObjectSerializer` uses the `itemsName` ("TagSet")
137+
as the key for the unwrapped array, producing `{ "TagSet": [items...] }`.
138+
139+
```ts models interface BlobTag
140+
/** A single tag */
141+
export interface BlobTag {
142+
/** Tag key */
143+
key: string;
144+
/** Tag value */
145+
value: string;
146+
}
147+
```
148+
149+
```ts models interface BlobTags
150+
/**
151+
* This file contains only generated model types and their (de)serializers.
152+
* Disable the following rules for internal models with '_' prefix and deserializers which require 'any' for raw JSON input.
153+
*/
154+
/* eslint-disable @typescript-eslint/naming-convention */
155+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
156+
/** Container for tags */
157+
export interface BlobTags {
158+
/** The tag set */
159+
blobTagSet: BlobTag[];
160+
}
161+
```
162+
163+
```ts models function blobTagXmlObjectSerializer
164+
export function blobTagXmlObjectSerializer(item: BlobTag): XmlSerializedObject {
165+
return { Key: item["key"], Value: item["value"] };
166+
}
167+
```
168+
169+
```ts models function blobTagsXmlSerializer
170+
export function blobTagsXmlSerializer(item: BlobTags): string {
171+
const properties: XmlPropertyMetadata[] = [
172+
{
173+
propertyName: "blobTagSet",
174+
xmlOptions: { name: "TagSet", unwrapped: true, itemsName: "TagSet" },
175+
type: "array",
176+
serializer: blobTagXmlObjectSerializer,
177+
},
178+
];
179+
return serializeToXml(item, properties, "Tags");
180+
}
181+
```
182+
183+
```ts models function blobTagsXmlObjectSerializer
184+
export function blobTagsXmlObjectSerializer(item: BlobTags): XmlSerializedObject {
185+
return { TagSet: item["blobTagSet"]?.map((i: any) => blobTagXmlObjectSerializer(i)) };
186+
}
187+
```

0 commit comments

Comments
 (0)