Skip to content

Commit 43ec459

Browse files
eladbmonadabot
andauthored
fix: structs can't parse json with null values (#7258)
Fixes #7257 by accepting `null` for any optional field in structs and converting `null` to `undefined` during parsing from JSON to structs. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [x] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --------- Signed-off-by: monada-bot[bot] <monabot@monada.co> Co-authored-by: monada-bot[bot] <monabot@monada.co>
1 parent bc74746 commit 43ec459

File tree

9 files changed

+119
-27
lines changed

9 files changed

+119
-27
lines changed

docs/api/05-language-reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,9 @@ let x = Contact.fromJson(p, unsafe: true);
418418
assert(x.last.len > 0); // RUNTIME ERROR
419419
```
420420
421+
> NOTE: `fromJson()` and `parseJson()` treats `null` values as `undefined` and therefore can be
422+
> assigned to optional fields.
423+
421424
##### 1.1.4.8 Serialization
422425
423426
The `Json.stringify(j: Json): str` static method can be used to serialize a `Json` as a string

docs/contributing/01-start-here/05-development.md

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,39 +98,50 @@ pnpm build
9898

9999
It will compile, lint, test and package all modules.
100100

101-
## 🏠 What's the recommended development workflow?
102-
:::info
103-
When testing your changes to Wing, locally it may be helpful to be able to easily invoke your local version of the Wing CLI.
104-
In which case adding a shell alias may be helpful for instance on Linux and Mac you could add:
105-
106-
`alias mywing=/<PATH_TO_WING_REPO>/packages/winglang/bin/wing` to your shell's rc file.
107-
:::
101+
## What's the recommended development workflow? 🏠
108102

109-
The `pnpm wing` command can be executed from the root of the repository in order to build and run the
110-
compiler, SDK (standard library) and the Wing CLI. Turbo is configured to make sure only the changed components are built
103+
The `pnpm wing` command can be executed from the *root of the repository* in order to build
104+
everything and run Wing CLI. Turbo is configured to make sure only the changed components are built
111105
every time.
112106

107+
108+
:::info
113109
To get full diagnostics, use these exports:
114110

115111
```sh
116112
export NODE_OPTIONS=--stack-trace-limit=100
117113
export RUST_BACKTRACE=full
118114
```
115+
:::
119116

120-
Or if you just want to compile your changes and run a local version of the Wing CLI:
117+
Now, you can edit a source file anywhere across the stack and run the compiler with arguments.
118+
For example:
121119

122120
```sh
123-
turbo compile -F winglang
121+
pnpm wing -- test tests/valid/captures.test.w
124122
```
125123

126-
Now, you can edit a source file anywhere across the stack and run the compiler with arguments.
127-
For example:
124+
This command runs the full Wing CLI with the given arguments. Turbo will ensure the CLI build is updated.
125+
126+
:::info
127+
When testing your changes to Wing locally it may be helpful to be able to easily invoke your local version of the Wing CLI.
128+
129+
First, you need to compile changes:
128130

129131
```sh
130-
pnpm wing -- test examples/tests/valid/captures.test.w
132+
npx turbo compile -F winglang
131133
```
132134

133-
This command runs the full Wing CLI with the given arguments. Turbo will ensure the CLI build is updated.
135+
Then, run the Wing CLI binary directly:
136+
137+
```sh
138+
./packages/winglang/bin/wing
139+
```
140+
141+
Pro tip: create a shell alias:
142+
143+
`alias mywing=/<PATH_TO_WING_REPO>/packages/winglang/bin/wing` to your shell's rc file.
144+
:::
134145

135146
## How is the repository structured?
136147

@@ -154,7 +165,7 @@ If you wish to install it manually, you may do so by running `scripts/setup_wasi
154165

155166
:::
156167

157-
## 🧪 How do I run tests?
168+
## How do I run tests? 🧪
158169

159170
End-to-end tests are hosted under `tools/hangar`. To get started, first ensure you can [build
160171
wing](#-how-do-i-build-wing).
@@ -167,6 +178,14 @@ turbo wing:e2e
167178

168179
(This is a helpful shortcut for `turbo test -F hangar`)
169180

181+
To run a single test, use the `wing test` from the root and reference the test file name:
182+
183+
For example:
184+
185+
```sh
186+
pnpm wing -- test tests/valid/optionals.test.w
187+
```
188+
170189
### Test Meta-Comments
171190

172191
In your wing files in `examples/tests/valid`, you can add a specially formatted comment to add additional information for hangar.
@@ -186,7 +205,7 @@ Currently, the only supported meta-comment for regular tests is `skipPlatforms`.
186205
This will skip the test on the given platforms when when running on CI. The current supported platforms are `win32`, `darwin`, and `linux`.
187206
This is useful if, for example, the test requires docker. In our CI only linux supports docker.
188207

189-
### Benchmarks
208+
## Performance Benchmarks
190209

191210
Benchmark files are located in `examples/tests/valid/benchmarks`. To run the benchmarks, run the following command from anywhere in the monorepo:
192211

@@ -256,7 +275,7 @@ highlight queries, run:
256275
turbo playground -F @winglang/tree-sitter-wing
257276
```
258277

259-
## 🔨 How do I build the VSCode extension?
278+
## How do I build the VSCode extension? 🔨
260279

261280
The VSCode extension is located in `packages/vscode-wing`. Most of the "logic" is in the language server, which
262281
is located in the Wing CLI at `packages/winglang/src/commands/lsp.ts`.
@@ -277,7 +296,7 @@ To modify the package.json, make sure to edit `.projenrc.ts` and rebuild.
277296

278297
Tip: if you want to print debug messages in your code while developing, you should use Rust's `dbg!` macro, instead of `print!` or `println!`.
279298

280-
## 🧹 How do I lint my code?
299+
## How do I lint my code? 🧹
281300

282301
To lint Rust code, you can run the `lint` target on the `wingc` or `wingii` projects:
283302

@@ -294,7 +313,7 @@ Lastly you can show linting errors in your IDE by enabling the following setting
294313
"rust-analyzer.check.command": "clippy",
295314
```
296315

297-
## 🏁 How do I add a quickstart template to the `wing` CLI?
316+
## How do I add a quickstart template to the `wing` CLI? 🏁
298317

299318
Adding a new template is straightforward!
300319

packages/@winglang/sdk/src/std/json_schema.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export class JsonSchema {
7777
const fields = extractFieldsFromSchema(this._rawSchema);
7878
// Filter rawParameters based on the schema
7979
const filteredParameters = filterParametersBySchema(fields, obj);
80-
return filteredParameters;
80+
81+
// Remove all `null` values (recursively)
82+
const cleanedParameters = removeNullValues(filteredParameters);
83+
return cleanedParameters;
8184
}
8285

8386
/** @internal */
@@ -103,3 +106,17 @@ export class JsonSchema {
103106
return JsonSchema._toInflightType(this._rawSchema);
104107
}
105108
}
109+
110+
function removeNullValues(obj: any): any {
111+
if (typeof obj === "object" && !Array.isArray(obj)) {
112+
const result: any = {};
113+
for (const [key, value] of Object.entries(obj)) {
114+
if (value !== null) {
115+
result[key] = removeNullValues(value);
116+
}
117+
}
118+
return result;
119+
}
120+
121+
return obj;
122+
}

packages/@winglang/wingc/src/json_schema_generator.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ impl JsonSchemaGenerator {
9999
code.append("}");
100100
code.to_string()
101101
}
102-
Type::Optional(ref t) => self.get_struct_schema_field(t, docs),
102+
Type::Optional(ref t) => format!(
103+
"{{oneOf:[{{type:\"null\"}},{}]}}",
104+
self.get_struct_schema_field(t, docs)
105+
),
103106
Type::Json(_) => match docs {
104107
Some(docs) => format!(
105108
"{{type:[\"object\",\"string\",\"boolean\",\"number\",\"array\"],description:\"{}\"}}",

tests/valid/optionals.test.w

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,37 @@ assert(maybeX! == 0);
256256

257257
let maybeY: str? = "";
258258
assert(maybeY! == "");
259+
260+
// ------------------------------------------------------------------------------------------------
261+
// verify that `null` is treated as undefined/nil
262+
// https://github.com/winglang/wing/issues/7257
263+
264+
struct S1 {
265+
x: str?;
266+
}
267+
268+
log(S1.schema().asStr());
269+
270+
let s9 = S1.parseJson("\{\"x\": null}");
271+
assert(s9.x == nil);
272+
273+
struct S2 {
274+
y: S1?;
275+
}
276+
277+
log(S2.schema().asStr());
278+
let s10 = S2.parseJson("\{\"y\": null}");
279+
assert(s10.y == nil);
280+
281+
let s11 = S2.parseJson("\{\"y\": \{\"x\": null\}}");
282+
assert(s11.y?.x == nil);
283+
284+
struct S3 {
285+
arr: Array<str>?;
286+
map: Map<str>?;
287+
}
288+
289+
let s12 = S3.parseJson("\{\"arr\": null,\"map\": null}");
290+
assert(s12.arr == nil);
291+
assert(s12.map == nil);
292+
// ------------------------------------------------------------------------------------------------

tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ class $Root extends $stdlib.std.Resource {
159159
let $preflightTypesMap = {};
160160
const cloud = $stdlib.cloud;
161161
const Person = $stdlib.std.Struct._createJsonSchema({$id:"/Person",type:"object",properties:{age:{type:"number"},name:{type:"string"},},required:["age","name",],description:""});
162+
const S1 = $stdlib.std.Struct._createJsonSchema({$id:"/S1",type:"object",properties:{x:{oneOf:[{type:"null"},{type:"string"}]},},required:[],description:""});
163+
const S2 = $stdlib.std.Struct._createJsonSchema({$id:"/S2",type:"object",properties:{y:{oneOf:[{type:"null"},{type:"object",properties:{x:{oneOf:[{type:"null"},{type:"string"}]},},required:[],description:""}]},},required:[],description:""});
164+
const S3 = $stdlib.std.Struct._createJsonSchema({$id:"/S3",type:"object",properties:{arr:{oneOf:[{type:"null"},{type:"array",items:{type:"string"}}]},map:{oneOf:[{type:"null"},{type:"object",patternProperties: {".*":{type:"string"}},}]},},required:[],description:""});
162165
$helpers.nodeof(this).root.$preflightTypesMap = $preflightTypesMap;
163166
class Super extends $stdlib.std.Resource {
164167
constructor($scope, $id, ) {
@@ -506,6 +509,17 @@ class $Root extends $stdlib.std.Resource {
506509
$helpers.assert($helpers.eq($helpers.unwrap(maybeX), 0), "maybeX! == 0");
507510
const maybeY = "";
508511
$helpers.assert($helpers.eq($helpers.unwrap(maybeY), ""), "maybeY! == \"\"");
512+
console.log(($macros.__Struct_schema(false, S1, ).asStr()));
513+
const s9 = $macros.__Struct_parseJson(false, S1, "{\"x\": null}");
514+
$helpers.assert($helpers.eq(s9.x, undefined), "s9.x == nil");
515+
console.log(($macros.__Struct_schema(false, S2, ).asStr()));
516+
const s10 = $macros.__Struct_parseJson(false, S2, "{\"y\": null}");
517+
$helpers.assert($helpers.eq(s10.y, undefined), "s10.y == nil");
518+
const s11 = $macros.__Struct_parseJson(false, S2, "{\"y\": {\"x\": null\}}");
519+
$helpers.assert($helpers.eq(s11.y?.x, undefined), "s11.y?.x == nil");
520+
const s12 = $macros.__Struct_parseJson(false, S3, "{\"arr\": null,\"map\": null}");
521+
$helpers.assert($helpers.eq(s12.arr, undefined), "s12.arr == nil");
522+
$helpers.assert($helpers.eq(s12.map, undefined), "s12.map == nil");
509523
}
510524
}
511525
const $APP = $PlatformManager.createApp({ outdir: $outdir, name: "optionals.test", rootConstruct: $Root, isTestEnvironment: $wing_is_test, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] });

tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_test_sim.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## stdout.log
44
```log
5+
{"$id":"/S1","type":"object","properties":{"x":{"oneOf":[{"type":"null"},{"type":"string"}]}},"required":[],"description":""}
6+
{"$id":"/S2","type":"object","properties":{"y":{"oneOf":[{"type":"null"},{"type":"object","properties":{"x":{"oneOf":[{"type":"null"},{"type":"string"}]}},"required":[],"description":""}]}},"required":[],"description":""}
57
pass ─ optionals.test.wsim » root/Default/test:t
68
79
Tests 1 passed (1)

tools/hangar/__snapshots__/test_corpus/valid/parameters/simple/parameters.test.w_compile_tf-aws.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class $Root extends $stdlib.std.Resource {
3636
super($scope, $id);
3737
$helpers.nodeof(this).root.$preflightTypesMap = { };
3838
let $preflightTypesMap = {};
39-
const MyParams = $stdlib.std.Struct._createJsonSchema({$id:"/MyParams",type:"object",properties:{foo:{type:"string"},meaningOfLife:{type:"number"},},required:["meaningOfLife",],description:""});
39+
const MyParams = $stdlib.std.Struct._createJsonSchema({$id:"/MyParams",type:"object",properties:{foo:{oneOf:[{type:"null"},{type:"string"}]},meaningOfLife:{type:"number"},},required:["meaningOfLife",],description:""});
4040
$helpers.nodeof(this).root.$preflightTypesMap = $preflightTypesMap;
4141
const myParams = $macros.__Struct_fromJson(false, MyParams, ($helpers.nodeof(this).app.parameters.read({ schema: $macros.__Struct_schema(false, MyParams, ) })));
4242
{

tools/hangar/__snapshots__/test_corpus/valid/struct_from_json.test.w_compile_tf-aws.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,11 @@ class $Root extends $stdlib.std.Resource {
199199
const otherExternalStructs = $helpers.bringJs(`${__dirname}/preflight.structs2-2.cjs`, $preflightTypesMap);
200200
const Bar = $stdlib.std.Struct._createJsonSchema({$id:"/Bar",type:"object",properties:{b:{type:"number"},f:{type:"string"},},required:["b","f",],description:""});
201201
const Foo = $stdlib.std.Struct._createJsonSchema({$id:"/Foo",type:"object",properties:{f:{type:"string"},},required:["f",],description:""});
202-
const Foosible = $stdlib.std.Struct._createJsonSchema({$id:"/Foosible",type:"object",properties:{f:{type:"string"},},required:[],description:""});
202+
const Foosible = $stdlib.std.Struct._createJsonSchema({$id:"/Foosible",type:"object",properties:{f:{oneOf:[{type:"null"},{type:"string"}]},},required:[],description:""});
203203
const MyStruct = $stdlib.std.Struct._createJsonSchema({$id:"/MyStruct",type:"object",properties:{color:{type:"string",enum:["red", "green", "blue"],description:"color docs\n@example Color.red"},m1:{type:"object",properties:{val:{type:"number",description:"val docs"},},required:["val",],description:"m1 docs"},m2:{type:"object",properties:{val:{type:"string"},},required:["val",],description:"m2 docs"},},required:["color","m1","m2",],description:"MyStruct docs\n@foo bar"});
204-
const Student = $stdlib.std.Struct._createJsonSchema({$id:"/Student",type:"object",properties:{additionalData:{type:["object","string","boolean","number","array"]},advisor:{type:"object",properties:{dob:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},employeeID:{type:"string"},firstName:{type:"string"},lastName:{type:"string"},},required:["dob","employeeID","firstName","lastName",],description:""},coursesTaken:{type:"array",items:{type:"object",properties:{course:{type:"object",properties:{credits:{type:"number"},name:{type:"string"},},required:["credits","name",],description:""},dateTaken:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},grade:{type:"string"},},required:["course","dateTaken","grade",],description:""}},dob:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},enrolled:{type:"boolean"},enrolledCourses:{type:"array",uniqueItems:true,items:{type:"object",properties:{credits:{type:"number"},name:{type:"string"},},required:["credits","name",],description:""}},firstName:{type:"string"},lastName:{type:"string"},schoolId:{type:"string"},},required:["dob","enrolled","firstName","lastName","schoolId",],description:""});
205-
const cloud_ApiResponse = $stdlib.std.Struct._createJsonSchema({$id:"/ApiResponse",type:"object",properties:{body:{type:"string",description:"The response\'s body.\n@default - no body\n@stability experimental"},headers:{type:"object",patternProperties: {".*":{type:"string"}},description:"The response\'s headers.\n@default {}\n@stability experimental",},status:{type:"number",description:"The response\'s status code.\n@default 200\n@stability experimental"},},required:[],description:"Shape of a response from a inflight handler.\n@stability experimental"});
206-
const cloud_CounterProps = $stdlib.std.Struct._createJsonSchema({$id:"/CounterProps",type:"object",properties:{initial:{type:"number",description:"The initial value of the counter.\n@default 0\n@stability experimental"},},required:[],description:"Options for `Counter`.\n@stability experimental"});
204+
const Student = $stdlib.std.Struct._createJsonSchema({$id:"/Student",type:"object",properties:{additionalData:{oneOf:[{type:"null"},{type:["object","string","boolean","number","array"]}]},advisor:{oneOf:[{type:"null"},{type:"object",properties:{dob:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},employeeID:{type:"string"},firstName:{type:"string"},lastName:{type:"string"},},required:["dob","employeeID","firstName","lastName",],description:""}]},coursesTaken:{oneOf:[{type:"null"},{type:"array",items:{type:"object",properties:{course:{type:"object",properties:{credits:{type:"number"},name:{type:"string"},},required:["credits","name",],description:""},dateTaken:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},grade:{type:"string"},},required:["course","dateTaken","grade",],description:""}}]},dob:{type:"object",properties:{day:{type:"number"},month:{type:"number"},year:{type:"number"},},required:["day","month","year",],description:""},enrolled:{type:"boolean"},enrolledCourses:{oneOf:[{type:"null"},{type:"array",uniqueItems:true,items:{type:"object",properties:{credits:{type:"number"},name:{type:"string"},},required:["credits","name",],description:""}}]},firstName:{type:"string"},lastName:{type:"string"},schoolId:{type:"string"},},required:["dob","enrolled","firstName","lastName","schoolId",],description:""});
205+
const cloud_ApiResponse = $stdlib.std.Struct._createJsonSchema({$id:"/ApiResponse",type:"object",properties:{body:{oneOf:[{type:"null"},{type:"string",description:"The response\'s body.\n@default - no body\n@stability experimental"}]},headers:{oneOf:[{type:"null"},{type:"object",patternProperties: {".*":{type:"string"}},description:"The response\'s headers.\n@default {}\n@stability experimental",}]},status:{oneOf:[{type:"null"},{type:"number",description:"The response\'s status code.\n@default 200\n@stability experimental"}]},},required:[],description:"Shape of a response from a inflight handler.\n@stability experimental"});
206+
const cloud_CounterProps = $stdlib.std.Struct._createJsonSchema({$id:"/CounterProps",type:"object",properties:{initial:{oneOf:[{type:"null"},{type:"number",description:"The initial value of the counter.\n@default 0\n@stability experimental"}]},},required:[],description:"Options for `Counter`.\n@stability experimental"});
207207
const externalStructs_MyOtherStruct = $stdlib.std.Struct._createJsonSchema({$id:"/MyOtherStruct",type:"object",properties:{data:{type:"object",properties:{val:{type:"number",description:"val docs"},},required:["val",],description:"MyStruct docs in subdir"},},required:["data",],description:""});
208208
$helpers.nodeof(this).root.$preflightTypesMap = $preflightTypesMap;
209209
const Color =

0 commit comments

Comments
 (0)