Skip to content

Commit e955d18

Browse files
authored
fix(sfn): can't override toStateJson() from other languages (#24593)
If any part of a state's JSON representation is `null`, that value will be replaced by `undefined` when jsii sends data to the other language, resulting in a change of semantics. Multi-language APIs cannot differentiate between `null` and `undefined` as non-JS languages typically fail to distinguish between them... In order to address that, a `JsonNull` value was added which serializes to `null` (via Javascript's standard `toJSON` method), which must be used in such cases where `null` may need to cross the language boundary. The `JsonPath.DISCARD` value is now a string-token representation of the `JsonNull` instance. Fixes #14639 Fixes aws/jsii#3999 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 65693b1 commit e955d18

File tree

4 files changed

+56
-4
lines changed

4 files changed

+56
-4
lines changed

packages/@aws-cdk/aws-stepfunctions/lib/fields.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Token, IResolvable } from '@aws-cdk/core';
1+
import { Token, IResolvable, JsonNull } from '@aws-cdk/core';
22
import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, renderInExpression, jsonPathFromAny } from './private/json-path';
33

44
/**
@@ -9,9 +9,9 @@ import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject, rende
99
*/
1010
export class JsonPath {
1111
/**
12-
* Special string value to discard state input, output or result
12+
* Special string value to discard state input, output or result.
1313
*/
14-
public static readonly DISCARD = 'DISCARD';
14+
public static readonly DISCARD = Token.asString(JsonNull.INSTANCE, { displayHint: 'DISCARD (JSON `null`)' });
1515

1616
/**
1717
* Instead of using a literal string, get the value from a JSON path

packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Token } from '@aws-cdk/core';
12
import { IConstruct, Construct, Node } from 'constructs';
23
import { Condition } from '../condition';
34
import { FieldUtils, JsonPath } from '../fields';
@@ -579,7 +580,7 @@ export function renderJsonPath(jsonPath?: string): undefined | null | string {
579580
if (jsonPath === undefined) { return undefined; }
580581
if (jsonPath === JsonPath.DISCARD) { return null; }
581582

582-
if (!jsonPath.startsWith('$')) {
583+
if (!Token.isUnresolved(jsonPath) && !jsonPath.startsWith('$')) {
583584
throw new Error(`Expected JSON path to start with '$', got: ${jsonPath}`);
584585
}
585586
return jsonPath;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as cdk from '@aws-cdk/core';
2+
import { FakeTask } from './integ.state-machine-credentials';
3+
import { renderGraph } from './private/render-util';
4+
import { JsonPath } from '../lib';
5+
6+
test('JsonPath.DISCARD can be used to discard a state\'s output', () => {
7+
const stack = new cdk.Stack();
8+
9+
const task = new FakeTask(stack, 'my-state', {
10+
inputPath: JsonPath.DISCARD,
11+
outputPath: JsonPath.DISCARD,
12+
resultPath: JsonPath.DISCARD,
13+
});
14+
15+
expect(renderGraph(task)).toEqual({
16+
StartAt: 'my-state',
17+
States: {
18+
'my-state': {
19+
End: true,
20+
Type: 'Task',
21+
Resource: expect.any(String),
22+
Parameters: expect.any(Object),
23+
// The important bits:
24+
InputPath: null,
25+
OutputPath: null,
26+
ResultPath: null,
27+
},
28+
},
29+
});
30+
});

packages/@aws-cdk/core/lib/token.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,27 @@ export class Tokenization {
232232
}
233233
}
234234

235+
/**
236+
* An object which serializes to the JSON `null` literal, and which can safely
237+
* be passed across languages where `undefined` and `null` are not different.
238+
*/
239+
export class JsonNull {
240+
/** The canonical instance of `JsonNull`. */
241+
public static readonly INSTANCE = new JsonNull();
242+
243+
private constructor() { }
244+
245+
/** Obtains the JSON representation of this object (`null`) */
246+
public toJSON(): any {
247+
return null;
248+
}
249+
250+
/** Obtains the string representation of this object (`'null'`) */
251+
public toString(): string {
252+
return 'null';
253+
}
254+
}
255+
235256
/**
236257
* Options for the 'reverse()' operation
237258
*/

0 commit comments

Comments
 (0)