From 751ca0a442f2023cbf69fd3ac7db2fc0227fedae Mon Sep 17 00:00:00 2001 From: Maksim Terentev Date: Thu, 4 Jun 2026 11:25:53 -0700 Subject: [PATCH 1/3] Adds support for cases. That PR fixes behavior. Before that, the WSDL marshaller could resolve this only as two (or more) separate type lists. The following JSON object has two separate lists of As and Bs: Assuming we have the following definitions of some Document: <> // Old behavior { // Parent document JSON representation someOtherProperty: { ... SomeOtherType-type object ... }, a: [ { ... A-type object 1 ... }, { ... A-type object 2 ... }, ...], b: [ { ... B-type object 1 ... }, { ... B-type object 2 ... }, { ... B-type object 3 ... }, ...], } that resulted into: <> ... SomeOtherType properties ... ... A-type properties 1 ... ... A-type properties 2 ... ... B-type properties 1 ... ... B-type properties 2 ... ... B-type properties 3 ... In case we want to preserve the choice children order for the following JSON object: // New behavior { // Parent document JSON representation someOtherProperty: { ... SomeOtherType-type object ... }, '$sequence': [ // <- Note this special transparent key that wraps a flat array object a: { ... A-type object 1 ... }, b: { ... B-type object 1 ... }, b: { ... B-type object 2 ... }, a: { ... A-type object 2 ... }, b: { ... B-type object 3 ... }, ] } to be encoded as: <> ... SomeOtherType properties ... ... A-type properties 1 ... ... B-type properties 1 ... ... B-type properties 2 ... ... A-type properties 2 ... ... B-type properties 3 ... In case of interfere with $sequence property of some SOAP Service property, the special key name can be replaced with options arrayWithChoiceTag: string value. --- src/types.ts | 2 + src/wsdl/index.ts | 46 ++++---- test/wsdl-parse-test.js | 147 ++++++++++++++++++++++++++ test/wsdl/complex/mixed-sequence.wsdl | 127 ++++++++++++++++++++++ 4 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 test/wsdl/complex/mixed-sequence.wsdl diff --git a/src/types.ts b/src/types.ts index 896a0bf08..80396ab8b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,6 +99,8 @@ export interface IWsdlBaseOptions { preserveWhitespace?: boolean; /** provides support for nonstandard array semantics. If true, JSON arrays of the form {list: [{elem: 1}, {elem: 2}]} are marshalled into xml as 1 2. If false, marshalls into 1 2 . Default: true. */ namespaceArrayElements?: boolean; + /** provides support for array with choice semantics. If array key matches, JSON arrays of the form {$sequence: [{elem: 1}, {elem: 2}]} are marshalled into xml as 12. Default: $sequence. */ + arrayWithChoiceTag?: string; useEmptyTag?: boolean; strict?: boolean; /** custom HTTP headers to be sent on WSDL requests. */ diff --git a/src/wsdl/index.ts b/src/wsdl/index.ts index 68fc61705..f80ee00a2 100644 --- a/src/wsdl/index.ts +++ b/src/wsdl/index.ts @@ -750,31 +750,36 @@ export class WSDL { for (i = 0, n = obj.length; i < n; i++) { const item = obj[i]; - const arrayAttr = this.processAttributes(item, nsContext); + const isArrayWithChoiceTagContainer = name === this.options.arrayWithChoiceTag; + const arrayAttr = isArrayWithChoiceTagContainer ? '' : this.processAttributes(item, nsContext); const correctOuterNsPrefix = nonSubNameSpace || parentNsPrefix || ns; // using the parent namespace prefix if given const body = this.objectToXML(item, name, nsPrefix, nsURI, false, null, schemaObject, nsContext); - let openingTagParts = ['<', name, arrayAttr, xmlnsAttrib]; - if (!emptyNonSubNameSpaceForArray) { - openingTagParts = ['<', appendColon(correctOuterNsPrefix), name, arrayAttr, xmlnsAttrib]; - } - - if (body === '' && this.options.useEmptyTag) { - // Use empty (self-closing) tags if no contents - openingTagParts.push(' />'); - parts.push(openingTagParts.join('')); + if (isArrayWithChoiceTagContainer) { + parts.push(body); } else { - openingTagParts.push('>'); - if (this.options.namespaceArrayElements || i === 0) { - parts.push(openingTagParts.join('')); + let openingTagParts = ['<', name, arrayAttr, xmlnsAttrib]; + if (!emptyNonSubNameSpaceForArray) { + openingTagParts = ['<', appendColon(correctOuterNsPrefix), name, arrayAttr, xmlnsAttrib]; } - parts.push(body); - if (this.options.namespaceArrayElements || i === n - 1) { - if (emptyNonSubNameSpaceForArray) { - parts.push([''].join('')); - } else { - parts.push([''].join('')); + + if (body === '' && this.options.useEmptyTag) { + // Use empty (self-closing) tags if no contents + openingTagParts.push(' />'); + parts.push(openingTagParts.join('')); + } else { + openingTagParts.push('>'); + if (this.options.namespaceArrayElements || i === 0) { + parts.push(openingTagParts.join('')); + } + parts.push(body); + if (this.options.namespaceArrayElements || i === n - 1) { + if (emptyNonSubNameSpaceForArray) { + parts.push([''].join('')); + } else { + parts.push([''].join('')); + } } } } @@ -939,7 +944,7 @@ export class WSDL { } } - value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, null, nsContext); + value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, name === this.options.arrayWithChoiceTag ? schemaObject : null, nsContext); } } else { value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, null, nsContext); @@ -1178,6 +1183,7 @@ export class WSDL { } else { this.options.namespaceArrayElements = true; } + this.options.arrayWithChoiceTag = options.arrayWithChoiceTag || '$sequence'; // Allow any request headers to keep passing through this.options.wsdl_headers = options.wsdl_headers; diff --git a/test/wsdl-parse-test.js b/test/wsdl-parse-test.js index 87709f26f..d46fd0930 100644 --- a/test/wsdl-parse-test.js +++ b/test/wsdl-parse-test.js @@ -144,4 +144,151 @@ describe(__filename, function () { done(); }); }); + + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with custom $sequence', function (done) { + open_wsdl( + path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), + { + arrayWithChoiceTag: '$arrayChoice', + }, + function (err, def) { + if (err) { + return done(err); + } + + if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { + return done('Unable to find "getDataResponse" complex type'); + } + + var requestBody = { + getDataResult: { + a: 0, + b: 10, + $arrayChoice: [ + { + c: { + id: '1', + value1: 'test 1', + }, + }, + { + d: { + id: '3', + value2: 'test 3', + }, + }, + { + c: { + id: '2', + value1: 'test 2', + }, + }, + ], + }, + }; + + var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); + + /** + * Expected XML: + * + * + * 0 + * 10 + * + * 1 + * test 1 + * + * + * 3 + * test 3 + * + * + * 2 + * test 2 + * + * + * + */ + assert.strictEqual( + requestAsXML, + '0101test 13test 32test 2', + ); + + done(); + }, + ); + }); + + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with default $sequence', function (done) { + open_wsdl( + path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), + function (err, def) { + if (err) { + return done(err); + } + + if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { + return done('Unable to find "getDataResponse" complex type'); + } + + var requestBody = { + getDataResult: { + a: 0, + b: 10, + $sequence: [ + { + c: { + id: '1', + value1: 'test 1', + }, + }, + { + d: { + id: '3', + value2: 'test 3', + }, + }, + { + c: { + id: '2', + value1: 'test 2', + }, + }, + ], + }, + }; + + var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); + + /** + * Expected XML: + * + * + * 0 + * 10 + * + * 1 + * test 1 + * + * + * 3 + * test 3 + * + * + * 2 + * test 2 + * + * + * + */ + assert.strictEqual( + requestAsXML, + '0101test 13test 32test 2', + ); + + done(); + }, + ); + }); }); diff --git a/test/wsdl/complex/mixed-sequence.wsdl b/test/wsdl/complex/mixed-sequence.wsdl new file mode 100644 index 000000000..bcae576d8 --- /dev/null +++ b/test/wsdl/complex/mixed-sequence.wsdl @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cd9bcd84f2e0c0f9ac81d52398f1b46ba08d8d29 Mon Sep 17 00:00:00 2001 From: Maksim Terentev Date: Thu, 18 Jun 2026 02:09:19 -0700 Subject: [PATCH 2/3] Response to code review. --- Readme.md | 1 + src/types.ts | 2 +- src/wsdl/index.ts | 2 +- test/wsdl-parse-test.js | 38 +++++++++++++++++--------------------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Readme.md b/Readme.md index fa06680c1..1baa06e31 100644 --- a/Readme.md +++ b/Readme.md @@ -137,6 +137,7 @@ Creates a new SOAP client from a WSDL URL. Also supports a local filesystem path - `overridePromiseSuffix` (_string_): Override the default method name suffix of WSDL operations for Promise-based methods. If any WSDL operation name ends with `Async', you must use this option. (**Default:** `Async`) - `normalizeNames` (_boolean_): Replace non-identifier characters (`[^a-z$_0-9]`) with `_` in WSDL operation names. Note: Clients using WSDLs with two operations like `soap:method` and `soap-method` will be overwritten. In this case, you must use bracket notation instead (`client['soap:method']()`). - `namespaceArrayElements` (_boolean_): Support non-standard array semantics. JSON arrays of the form `{list: [{elem: 1}, {elem: 2}]}` will be marshalled into XML as `1 2`. If `false`, it would be marshalled into ` 1 2 `. (**Default:** `true`) + - `arrayWithChoiceTag` (_string_): Support for mixed value sequence with choice semantics instead of ordered by type one. If `arrayWithChoiceTag` option is set and sequence key matches the option value, JSON arrays of the form `{$sequence: [{a: {elem: 1}}, {b: {elem: 2}}, {a: {elem: 3}}]}` (where `$sequence` is example value for the option) are marshaled into xml as `1 2 3` preserving the original JSON order. By default, if option is not set, it will be marshaled as `<$sequence> 1 3 2 `. (**Default:** Disabled if option is not set) - `stream` (_boolean_): Use streams to parse the XML SOAP responses. (**Default:** `false`) - `returnSaxStream` (_boolean_): Return the SAX stream, transferring responsibility of parsing XML to the end user. Only valid when the _stream_ option is set to `true`. (**Default:** `false`) - `parseReponseAttachments` (_boolean_): Treat response as multipart/related response with MTOM attachment. Reach attachments on the `lastResponseAttachments` property of SoapClient. (**Default:** `false`) diff --git a/src/types.ts b/src/types.ts index 80396ab8b..65e3423d9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,7 +99,7 @@ export interface IWsdlBaseOptions { preserveWhitespace?: boolean; /** provides support for nonstandard array semantics. If true, JSON arrays of the form {list: [{elem: 1}, {elem: 2}]} are marshalled into xml as 1 2. If false, marshalls into 1 2 . Default: true. */ namespaceArrayElements?: boolean; - /** provides support for array with choice semantics. If array key matches, JSON arrays of the form {$sequence: [{elem: 1}, {elem: 2}]} are marshalled into xml as 12. Default: $sequence. */ + /** provides support for sequence with choice semantics. If sequence key matches, JSON arrays of the form {$sequence: [{elem: 1}, {elem: 2}]} are marshalled into xml as 12 where $sequence is example value for the option. Disabled if option is not set. Example value: $sequence. */ arrayWithChoiceTag?: string; useEmptyTag?: boolean; strict?: boolean; diff --git a/src/wsdl/index.ts b/src/wsdl/index.ts index f80ee00a2..d705a2355 100644 --- a/src/wsdl/index.ts +++ b/src/wsdl/index.ts @@ -1183,7 +1183,7 @@ export class WSDL { } else { this.options.namespaceArrayElements = true; } - this.options.arrayWithChoiceTag = options.arrayWithChoiceTag || '$sequence'; + this.options.arrayWithChoiceTag = options.arrayWithChoiceTag; // Allow any request headers to keep passing through this.options.wsdl_headers = options.wsdl_headers; diff --git a/test/wsdl-parse-test.js b/test/wsdl-parse-test.js index d46fd0930..7adc4ec1a 100644 --- a/test/wsdl-parse-test.js +++ b/test/wsdl-parse-test.js @@ -145,11 +145,11 @@ describe(__filename, function () { }); }); - it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with custom $sequence', function (done) { + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with specified arrayWithChoiceTag tag key', function (done) { open_wsdl( path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), { - arrayWithChoiceTag: '$arrayChoice', + arrayWithChoiceTag: '$sequence', }, function (err, def) { if (err) { @@ -164,7 +164,7 @@ describe(__filename, function () { getDataResult: { a: 0, b: 10, - $arrayChoice: [ + $sequence: [ { c: { id: '1', @@ -220,7 +220,7 @@ describe(__filename, function () { ); }); - it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with default $sequence', function (done) { + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with omitted arrayWithChoiceTag tag key', function (done) { open_wsdl( path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), function (err, def) { @@ -236,25 +236,21 @@ describe(__filename, function () { getDataResult: { a: 0, b: 10, - $sequence: [ + c: [ { - c: { id: '1', value1: 'test 1', - }, }, { - d: { + id: '2', + value1: 'test 2', + } + ], + d: [ + { id: '3', value2: 'test 3', - }, - }, - { - c: { - id: '2', - value1: 'test 2', - }, - }, + } ], }, }; @@ -271,20 +267,20 @@ describe(__filename, function () { * 1 * test 1 * - * - * 3 - * test 3 - * * * 2 * test 2 * + * + * 3 + * test 3 + * * * */ assert.strictEqual( requestAsXML, - '0101test 13test 32test 2', + '0101test 12test 23test 3', ); done(); From a49a7c31e8601bc72fa78161fa726b9d85abdb51 Mon Sep 17 00:00:00 2001 From: Maksim Terentev Date: Sat, 20 Jun 2026 14:37:02 -0700 Subject: [PATCH 3/3] Linter fixes. --- test/wsdl-parse-test.js | 117 ++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/test/wsdl-parse-test.js b/test/wsdl-parse-test.js index 7adc4ec1a..3ce286850 100644 --- a/test/wsdl-parse-test.js +++ b/test/wsdl-parse-test.js @@ -221,70 +221,67 @@ describe(__filename, function () { }); it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with omitted arrayWithChoiceTag tag key', function (done) { - open_wsdl( - path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), - function (err, def) { - if (err) { - return done(err); - } + open_wsdl(path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), function (err, def) { + if (err) { + return done(err); + } - if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { - return done('Unable to find "getDataResponse" complex type'); - } + if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { + return done('Unable to find "getDataResponse" complex type'); + } - var requestBody = { - getDataResult: { - a: 0, - b: 10, - c: [ - { - id: '1', - value1: 'test 1', - }, - { - id: '2', - value1: 'test 2', - } - ], - d: [ - { - id: '3', - value2: 'test 3', - } - ], - }, - }; + var requestBody = { + getDataResult: { + a: 0, + b: 10, + c: [ + { + id: '1', + value1: 'test 1', + }, + { + id: '2', + value1: 'test 2', + }, + ], + d: [ + { + id: '3', + value2: 'test 3', + }, + ], + }, + }; - var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); + var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); - /** - * Expected XML: - * - * - * 0 - * 10 - * - * 1 - * test 1 - * - * - * 2 - * test 2 - * - * - * 3 - * test 3 - * - * - * - */ - assert.strictEqual( - requestAsXML, - '0101test 12test 23test 3', - ); + /** + * Expected XML: + * + * + * 0 + * 10 + * + * 1 + * test 1 + * + * + * 2 + * test 2 + * + * + * 3 + * test 3 + * + * + * + */ + assert.strictEqual( + requestAsXML, + '0101test 12test 23test 3', + ); - done(); - }, - ); + done(); + }); }); });