Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update JSON validation logic #47

Merged
merged 5 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -33,8 +33,7 @@
"dependencies": {
"@types/debug": "^4.1.7",
"debug": "^4.3.4",
"fast-deep-equal": "^3.1.3",
"superstruct": "^0.16.5"
"superstruct": "^0.16.7"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^2.0.3",
Expand Down
39 changes: 3 additions & 36 deletions src/__fixtures__/json.ts
Expand Up @@ -1112,22 +1112,11 @@ export const COMPLEX_OBJECT = {
t: [true, true, true],
f: [false, false, false],
nulls: [null, null, null],
undef: undefined,
mixed: [
null,
undefined,
null,
undefined,
null,
true,
null,
false,
null,
undefined,
],
undef: null,
mixed: [null, null, null, null, null, true, null, false, null, null],
inObject: {
valueOne: null,
valueTwo: undefined,
valueTwo: null,
t: true,
f: false,
},
Expand Down Expand Up @@ -1159,25 +1148,3 @@ export const ARRAY_OF_DIFFRENT_KINDS_OF_NUMBERS = [
-5e-11, 5e-9, 0.000000000001, -0.00000000009, 100000.00000008, -100.88888,
0.333, 1000000000000,
];

export const ARRAY_OF_MIXED_SPECIAL_OBJECTS = [
null,
undefined,
null,
undefined,
undefined,
undefined,
null,
null,
null,
undefined,
];

export const OBJECT_MIXED_WITH_UNDEFINED_VALUES = {
a: undefined,
b: 'b',
c: undefined,
d: 'd',
e: undefined,
f: 'f',
};
33 changes: 14 additions & 19 deletions src/json.test.ts
@@ -1,7 +1,6 @@
import * as superstructModule from 'superstruct';
import {
ARRAY_OF_DIFFRENT_KINDS_OF_NUMBERS,
ARRAY_OF_MIXED_SPECIAL_OBJECTS,
COMPLEX_OBJECT,
JSON_FIXTURES,
JSON_RPC_ERROR_FIXTURES,
Expand All @@ -12,7 +11,6 @@ import {
JSON_RPC_RESPONSE_FIXTURES,
JSON_RPC_SUCCESS_FIXTURES,
NON_SERIALIZABLE_NESTED_OBJECT,
OBJECT_MIXED_WITH_UNDEFINED_VALUES,
} from './__fixtures__';
import {
assertIsJsonRpcFailure,
Expand Down Expand Up @@ -522,24 +520,15 @@ describe('json', () => {
]);
});

it('should return true for serialization and 2 for a size when only one key with undefined value is provided', () => {
it('should return false for serialization and 0 for a size when only one key with undefined value is provided', () => {
const valueToSerialize = {
a: undefined,
};

expect(validateJsonAndGetSize(valueToSerialize)).toStrictEqual([true, 2]);
});

it('should return true for serialization and 25 for a size when some of the values are undefined', () => {
expect(
validateJsonAndGetSize(OBJECT_MIXED_WITH_UNDEFINED_VALUES),
).toStrictEqual([true, 25]);
});

it('should return true for serialization and 17 for a size with mixed null and undefined in an array', () => {
expect(
validateJsonAndGetSize(ARRAY_OF_MIXED_SPECIAL_OBJECTS),
).toStrictEqual([true, 51]);
expect(validateJsonAndGetSize(valueToSerialize)).toStrictEqual([
false,
0,
]);
});

it('should return true for serialization and 73 for a size, for an array of numbers', () => {
Expand All @@ -548,10 +537,10 @@ describe('json', () => {
).toStrictEqual([true, 73]);
});

it('should return true for serialization and 1259 for a size of a complex nested object', () => {
it('should return true for serialization and 1288 for a size of a complex nested object', () => {
expect(validateJsonAndGetSize(COMPLEX_OBJECT)).toStrictEqual([
true,
1259,
1288,
]);
});

Expand Down Expand Up @@ -611,6 +600,12 @@ describe('json', () => {
]);
});

it('returns true for serialization and 37 for a size when checking an array', () => {
expect(
validateJsonAndGetSize(['foo', 'bar', null, ['foo', 'bar', null]]),
).toStrictEqual([true, 37]);
});

it('should return true or false for validity depending on the test scenario from ECMA TC39 (test262)', () => {
// This test will perform a series of validation assertions.
// These tests are taken from ECMA TC39 (test262) test scenarios used
Expand Down Expand Up @@ -761,7 +756,7 @@ describe('json', () => {
expect(validateJsonAndGetSize(false, true)).toStrictEqual([true, 0]);
expect(validateJsonAndGetSize('str', true)).toStrictEqual([true, 0]);
expect(validateJsonAndGetSize(123, true)).toStrictEqual([true, 0]);
expect(validateJsonAndGetSize(undefined, true)).toStrictEqual([true, 0]);
expect(validateJsonAndGetSize(undefined, true)).toStrictEqual([false, 0]);

// Value: string escape ASCII
const charToJson = {
Expand Down
39 changes: 7 additions & 32 deletions src/json.ts
@@ -1,19 +1,16 @@
import deepEqual from 'fast-deep-equal';
import {
array,
assert,
boolean,
define,
Infer,
integer,
is,
lazy,
literal,
nullable,
number,
object,
omit,
optional,
record,
string,
Struct,
union,
Expand All @@ -37,15 +34,10 @@ function isErrorWithMessage(error: unknown): error is { message: string } {
return typeof error === 'object' && error !== null && 'message' in error;
}

// Note: This struct references itself, so TypeScript cannot infer the type.
export const JsonStruct: Struct<Json> = union([
literal(null),
boolean(),
number(),
string(),
lazy(() => array(JsonStruct)),
lazy(() => record(string(), JsonStruct)),
]);
export const JsonStruct = define<Json>('Json', (value) => {
const [isValid] = validateJsonAndGetSize(value, true);
return isValid;
});

/**
* Any JSON-compatible value.
Expand All @@ -65,11 +57,7 @@ export type Json =
* @returns Whether the value is valid JSON.
*/
export function isValidJson(value: unknown): value is Json {
try {
return deepEqual(value, JSON.parse(JSON.stringify(value)));
} catch (_) {
return false;
}
return is(value, JsonStruct);
}

/**
Expand Down Expand Up @@ -512,8 +500,7 @@ export function validateJsonAndGetSize(
skipSizing: boolean,
): [isValid: boolean, plainTextSizeInBytes: number] {
if (value === undefined) {
// Return zero for undefined, since these are omitted from JSON serialization
return [true, 0];
return [false, 0];
} else if (value === null) {
// Return already specified constant size for null (special object)
return [true, skipSizing ? 0 : JsonSize.Null];
Expand Down Expand Up @@ -600,18 +587,6 @@ export function validateJsonAndGetSize(
return 0;
}

// If the size is 0, the value is undefined and undefined in an array
// when serialized will be replaced with null
if (size === 0 && Array.isArray(value)) {
size = JsonSize.Null;
}

// If the size is 0, that means the object is undefined and
// the rest of the object structure will be omitted
if (size === 0) {
return sum;
}

// Objects will have be serialized with "key": value,
// therefore we include the key in the calculation here
const keySize = Array.isArray(value)
Expand Down
11 changes: 5 additions & 6 deletions yarn.lock
Expand Up @@ -871,14 +871,13 @@ __metadata:
eslint-plugin-jsdoc: ^36.1.0
eslint-plugin-node: ^11.1.0
eslint-plugin-prettier: ^3.3.1
fast-deep-equal: ^3.1.3
jest: ^28.1.0
json-bigint: ^1.0.0
prettier: ^2.2.1
prettier-plugin-packagejson: ^2.2.11
rimraf: ^3.0.2
stdio-mock: ^1.2.0
superstruct: ^0.16.5
superstruct: ^0.16.7
ts-jest: ^28.0.8
tsd: ^0.24.1
typedoc: ^0.23.10
Expand Down Expand Up @@ -5782,10 +5781,10 @@ __metadata:
languageName: node
linkType: hard

"superstruct@npm:^0.16.5":
version: 0.16.5
resolution: "superstruct@npm:0.16.5"
checksum: 9f843c38695b584a605ae9b028629de18a85bd0dca0e9449b4ab98bb7b9ac3d82599870acbab9fbd2ee454c6b187af7e61562e252dfadabd974191ab4ab2e3ce
"superstruct@npm:^0.16.7":
version: 0.16.7
resolution: "superstruct@npm:0.16.7"
checksum: c8c855ff6945da8a41048c6d236de7b1af5d4d9c31742b3ee54d65647c31597488620281f65e095d5efc9e2fbdaad529b8c8f2506c12569d428467c835a21477
languageName: node
linkType: hard

Expand Down