Skip to content

Commit 66e8a42

Browse files
gaurav0107claude
andcommitted
[POWERSHELL] fix: single-quote DTO property names to prevent $-variable interpolation
PowerShell treats `$` inside double-quoted strings as the sigil for variable interpolation, so `"$foo" = ${Foo}` inside a hash literal becomes `<value-of-$foo> = <value-of-Foo>`. When an OpenAPI property name starts with (or contains) `$` — e.g. `$type` from C# polymorphic payloads or `$ref` / `$schema` from JSON Schema — the generated Initialize- and ConvertFrom- commandlets emit invalid PSCustomObject hash keys and empty regex patterns, breaking DTO (de)serialization. Swap all user-property-name emissions in `model_simple.mustache` from double-quoted to single-quoted literals so baseName is preserved verbatim: - `$PSO = [PSCustomObject]@{ '<baseName>' = ${<name>} }` (3 sites) - `$AllProperties = ('<baseName>', ...)` (1 site) - `-match '<baseName>'` and `.Properties['<baseName>']` (4 sites) Fixes: #23535 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6be478d commit 66e8a42

2 files changed

Lines changed: 95 additions & 9 deletions

File tree

modules/openapi-generator/src/main/resources/powershell/model_simple.mustache

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function Initialize-{{{apiNamePrefix}}}{{{classname}}} {
122122
$PSO = [PSCustomObject]@{
123123
{{=<< >>=}}
124124
<<#allVars>>
125-
"<<baseName>>" = ${<<name>>}
125+
'<<baseName>>' = ${<<name>>}
126126
<</allVars>>
127127
<<={{ }}=>>
128128
}
@@ -184,7 +184,7 @@ function Initialize-{{{apiNamePrefix}}}{{{classname}}} {
184184
{{=<< >>=}}
185185
<<#allVars>>
186186
<<^isReadOnly>>
187-
"<<baseName>>" = ${<<name>>}
187+
'<<baseName>>' = ${<<name>>}
188188
<</isReadOnly>>
189189
<</allVars>>
190190
<<={{ }}=>>
@@ -228,7 +228,7 @@ function ConvertFrom-{{{apiNamePrefix}}}JsonTo{{{classname}}} {
228228
{{/isAdditionalPropertiesTrue}}
229229

230230
# check if Json contains properties not defined in {{{apiNamePrefix}}}{{{classname}}}
231-
$AllProperties = ({{#allVars}}"{{{baseName}}}"{{^-last}}, {{/-last}}{{/allVars}})
231+
$AllProperties = ({{#allVars}}'{{{baseName}}}'{{^-last}}, {{/-last}}{{/allVars}})
232232
foreach ($name in $JsonParameters.PsObject.Properties.Name) {
233233
{{^isAdditionalPropertiesTrue}}
234234
if (!($AllProperties.Contains($name))) {
@@ -250,29 +250,29 @@ function ConvertFrom-{{{apiNamePrefix}}}JsonTo{{{classname}}} {
250250
}
251251

252252
{{/-first}}
253-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "{{{baseName}}}"))) {
253+
if (!([bool]($JsonParameters.PSobject.Properties.name -match '{{{baseName}}}'))) {
254254
throw "Error! JSON cannot be serialized due to the required property '{{{baseName}}}' missing."
255255
} else {
256-
${{name}} = $JsonParameters.PSobject.Properties["{{{baseName}}}"].value
256+
${{name}} = $JsonParameters.PSobject.Properties['{{{baseName}}}'].value
257257
}
258258

259259
{{/requiredVars}}
260260
{{#optionalVars}}
261-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "{{{baseName}}}"))) { #optional property not found
261+
if (!([bool]($JsonParameters.PSobject.Properties.name -match '{{{baseName}}}'))) { #optional property not found
262262
${{name}} = $null
263263
} else {
264-
${{name}} = $JsonParameters.PSobject.Properties["{{{baseName}}}"].value
264+
${{name}} = $JsonParameters.PSobject.Properties['{{{baseName}}}'].value
265265
}
266266

267267
{{/optionalVars}}
268268
$PSO = [PSCustomObject]@{
269269
{{=<< >>=}}
270270
<<#allVars>>
271-
"<<baseName>>" = ${<<name>>}
271+
'<<baseName>>' = ${<<name>>}
272272
<</allVars>>
273273
<<={{ }}=>>
274274
{{#isAdditionalPropertiesTrue}}
275-
"AdditionalProperties" = ${{{apiNamePrefix}}}{{{classname}}}AdditionalProperties
275+
'AdditionalProperties' = ${{{apiNamePrefix}}}{{{classname}}}AdditionalProperties
276276
{{/isAdditionalPropertiesTrue}}
277277
}
278278

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.powershell;
19+
20+
import io.swagger.parser.OpenAPIParser;
21+
import io.swagger.v3.oas.models.OpenAPI;
22+
import io.swagger.v3.parser.core.models.ParseOptions;
23+
import org.openapitools.codegen.ClientOptInput;
24+
import org.openapitools.codegen.CodegenConstants;
25+
import org.openapitools.codegen.DefaultGenerator;
26+
import org.openapitools.codegen.languages.PowerShellClientCodegen;
27+
import org.testng.annotations.Test;
28+
29+
import java.io.File;
30+
import java.io.IOException;
31+
import java.nio.file.Files;
32+
import java.nio.file.Paths;
33+
34+
import static org.openapitools.codegen.TestUtils.assertFileContains;
35+
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
36+
37+
public class PowerShellClientCodegenTest {
38+
39+
/**
40+
* Regression test for <a href="https://github.com/OpenAPITools/openapi-generator/issues/23535">#23535</a>.
41+
*
42+
* PowerShell treats {@code $} inside double-quoted strings as the sigil for
43+
* variable interpolation, so hash-table keys emitted as {@code "$foo"} get
44+
* rewritten at runtime to the value of {@code $foo} (usually empty). This
45+
* produces invalid DTO commandlets whenever an OpenAPI property name starts
46+
* with (or contains) {@code $}. The {@code model_simple.mustache} template
47+
* now emits single-quoted keys so the literal baseName is preserved.
48+
*/
49+
@Test
50+
public void dollarSignPropertyNamesAreSingleQuoted() throws IOException {
51+
File output = Files.createTempDirectory("test-powershell-23535").toFile().getCanonicalFile();
52+
output.deleteOnExit();
53+
String outputPath = output.getAbsolutePath().replace('\\', '/');
54+
55+
OpenAPI openAPI = new OpenAPIParser()
56+
.readLocation("src/test/resources/3_0/dollar-in-names-pull14359.yaml", null, new ParseOptions())
57+
.getOpenAPI();
58+
59+
PowerShellClientCodegen codegen = new PowerShellClientCodegen();
60+
codegen.setOutputDir(output.getAbsolutePath());
61+
62+
ClientOptInput input = new ClientOptInput();
63+
input.openAPI(openAPI);
64+
input.config(codegen);
65+
66+
DefaultGenerator generator = new DefaultGenerator();
67+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
68+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
69+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
70+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
71+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
72+
generator.opts(input).generate();
73+
74+
// The PowerShell codegen sanitises "$DollarModel$" to "DollarModel",
75+
// so the emitted model lives at src/<package>/Model/DollarModel.ps1.
76+
java.nio.file.Path dollarModelPs1 = Paths.get(outputPath + "/src/PSOpenAPITools/Model/DollarModel.ps1");
77+
78+
// Property names containing `$` must be single-quoted so PowerShell does
79+
// not attempt variable interpolation.
80+
assertFileContains(dollarModelPs1, "'$dollarValue$'");
81+
82+
// The previous double-quoted emission must no longer appear anywhere in
83+
// the generated model file.
84+
assertFileNotContains(dollarModelPs1, "\"$dollarValue$\" = ");
85+
}
86+
}

0 commit comments

Comments
 (0)