From 8bfb1e33498ac61e9ce51fe4abb88ddfbd720e6e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 31 Oct 2020 17:37:19 -0400 Subject: [PATCH 1/9] Add JSON types Define explicit types for legal plain JSON values to avoid double-deserialization shenanigans. Fixes #59. --- from_json/as.ts | 9 ++++++-- from_json/as_test.ts | 4 ++-- from_json/date.ts | 4 ++-- serializable.ts | 46 +++++++++++++++++++++++++------------- serialize_property_test.ts | 8 ++++--- to_json/recursive.ts | 4 ++-- 6 files changed, 48 insertions(+), 27 deletions(-) diff --git a/from_json/as.ts b/from_json/as.ts index dad3c1b9..29ff5d2e 100644 --- a/from_json/as.ts +++ b/from_json/as.ts @@ -1,10 +1,15 @@ // Copyright 2018-2020 Gamebridge.ai authors. All rights reserved. MIT license. -import { FromJsonStrategy, Serializable } from "../serializable.ts"; +import { + FromJsonStrategy, + JsonObject, + JsonValue, + Serializable, +} from "../serializable.ts"; /** revive data using `fromJson` on a subclass type */ export function fromJsonAs( type: T & { new (): Serializable }, ): FromJsonStrategy { - return (value: T) => new type().fromJson(value); + return (value: JsonValue) => new type().fromJson(value as JsonObject); } diff --git a/from_json/as_test.ts b/from_json/as_test.ts index b1261aec..c30ffb68 100644 --- a/from_json/as_test.ts +++ b/from_json/as_test.ts @@ -13,8 +13,8 @@ test({ test = true; } const testObj = new Test(); - assertEquals(fromJsonAs(Test)(testObj).test, testObj.test); - assert(fromJsonAs(Test)(testObj) instanceof Test); + assertEquals(fromJsonAs(Test)({ test: true }).test, testObj.test); + assert(fromJsonAs(Test)({ test: true }) instanceof Test); }, }); diff --git a/from_json/date.ts b/from_json/date.ts index 243f169a..0b0321f0 100644 --- a/from_json/date.ts +++ b/from_json/date.ts @@ -1,10 +1,10 @@ // Copyright 2018-2020 Gamebridge.ai authors. All rights reserved. MIT license. -import { FromJsonStrategy } from "../serializable.ts"; +import { FromJsonStrategy, JsonValue } from "../serializable.ts"; /** allows authors to pass a regex to parse as a date */ export function createDateStrategy(regex: RegExp): FromJsonStrategy { - return (value: any): any | Date => { + return (value: JsonValue): any | Date => { return typeof value === "string" && regex.exec(value) ? new Date(value) : value; diff --git a/serializable.ts b/serializable.ts index ef783cd5..96bff1c1 100644 --- a/serializable.ts +++ b/serializable.ts @@ -4,6 +4,18 @@ import { SerializePropertyOptionsMap } from "./serialize_property_options_map.ts import { defaultToJson } from "./to_json/default.ts"; import { recursiveToJson } from "./to_json/recursive.ts"; +/** A JSON object where each property value is a simple JSON value. */ +export type JsonObject = { [key: string]: JsonValue }; + +/** A property value in a JSON object. */ +export type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | JsonObject; + /** Adds methods for serialization */ export abstract class Serializable { public toJson(): string { @@ -11,17 +23,17 @@ export abstract class Serializable { } public fromJson(): this; public fromJson(json: string): this; - public fromJson(json: Record): this; - public fromJson(json: string | Record = {}): this { + public fromJson(json: JsonObject): this; + public fromJson(json: string | JsonObject = {}): this { return fromJson(this, json); } } /** Functions used when hydrating data */ -export type FromJsonStrategy = (value: any) => any; +export type FromJsonStrategy = (value: JsonValue) => any; /** Functions used when dehydrating data */ -export type ToJsonStrategy = (value: any) => any; +export type ToJsonStrategy = (value: any) => JsonValue; /** options to use when (de)serializing values */ export class SerializePropertyOptions { @@ -56,13 +68,15 @@ export class SerializePropertyOptions { * Converts value from functions provided as parameters */ export function composeStrategy( - ...fns: - | (FromJsonStrategy | FromJsonStrategy[])[] - | (ToJsonStrategy | ToJsonStrategy[])[] -): FromJsonStrategy | ToJsonStrategy { - return (val: unknown): unknown => + ...fns: (FromJsonStrategy | FromJsonStrategy[])[] +): FromJsonStrategy; +export function composeStrategy( + ...fns: (ToJsonStrategy | ToJsonStrategy[])[] +): ToJsonStrategy; +export function composeStrategy(...fns: any): any { + return (val: any): any => fns.flat().reduce( - (acc: unknown, f: FromJsonStrategy | ToJsonStrategy) => f(acc), + (acc: any, f: FromJsonStrategy | ToJsonStrategy) => f(acc), val, ); } @@ -82,7 +96,7 @@ const ERROR_MESSAGE_MISSING_PROPERTIES_MAP = /** Converts to object using mapped keys */ export function toPojo( context: Record, -): Record { +): JsonObject { const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get( context?.constructor?.prototype, ); @@ -93,7 +107,7 @@ export function toPojo( ?.prototype}`, ); } - const record: Record = {}; + const record: JsonObject = {}; for ( let { propertyKey, @@ -130,16 +144,16 @@ function toJson(context: T): string { /** Convert from object/string to mapped object on the context */ function fromJson(context: Serializable, json: string): T; -function fromJson(context: Serializable, json: Record): T; +function fromJson(context: Serializable, json: JsonObject): T; function fromJson( context: Serializable, - json: string | Record, + json: string | JsonObject, ): T; function fromJson( context: Serializable, - json: string | Record, + json: string | JsonObject, ): T { const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get( context?.constructor?.prototype, @@ -158,7 +172,7 @@ function fromJson( JSON.parse( _json, /** Processes the value through the provided or default `fromJsonStrategy` */ - function revive(key: string, value: unknown): unknown { + function revive(key: string, value: JsonValue): unknown { // After the last iteration of the fromJsonStrategy a function // will be called one more time with a empty string key if (key === "") { diff --git a/serialize_property_test.ts b/serialize_property_test.ts index e7a3e725..ae83a3cd 100644 --- a/serialize_property_test.ts +++ b/serialize_property_test.ts @@ -7,7 +7,7 @@ import { assertStrictEquals, fail, } from "./test_deps.ts"; -import { Serializable } from "./serializable.ts"; +import { JsonObject, JsonValue, Serializable } from "./serializable.ts"; import { SerializeProperty, ERROR_MESSAGE_SYMBOL_PROPERTY_NAME, @@ -216,7 +216,8 @@ test({ } class Test extends Serializable { @SerializeProperty({ - fromJsonStrategy: (v: OtherClass) => new OtherClass().fromJson(v), + fromJsonStrategy: (v: JsonValue) => + new OtherClass().fromJson(v as JsonObject), }) array!: OtherClass[]; } @@ -348,7 +349,8 @@ test({ class Test2 extends Serializable { @SerializeProperty({ serializedKey: "serialize_me_2", - fromJsonStrategy: (json) => new Test1().fromJson(json), + fromJsonStrategy: (json: JsonValue) => + new Test1().fromJson(json as JsonObject), }) nested!: Test1; } diff --git a/to_json/recursive.ts b/to_json/recursive.ts index a85e7c2f..8055cc71 100644 --- a/to_json/recursive.ts +++ b/to_json/recursive.ts @@ -1,8 +1,8 @@ // Copyright 2018-2020 Gamebridge.ai authors. All rights reserved. MIT license. -import { toPojo, Serializable } from "../serializable.ts"; +import { toPojo, Serializable, JsonObject } from "../serializable.ts"; /** Recursively serialize a serializable class */ -export function recursiveToJson(value: Serializable): any { +export function recursiveToJson(value: Serializable): JsonObject { return toPojo(value); } From 8fe071e5a125b69b34591c7e02f0d1dcea708fa6 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 22:57:10 -0500 Subject: [PATCH 2/9] Fix semantic merge conflicts for JsonObject --- from_json/as.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/from_json/as.ts b/from_json/as.ts index 871856f9..4b1fb762 100644 --- a/from_json/as.ts +++ b/from_json/as.ts @@ -11,10 +11,10 @@ import { export function fromJsonAs( type: T & { new (): Serializable }, ): FromJsonStrategy { - return (value: T) => { + return (value: JsonValue) => { if (Array.isArray(value)) { - return value.map((item) => new type().fromJson(item)); + return value.map((item) => new type().fromJson(item as JsonObject)); } - return new type().fromJson(value); + return new type().fromJson(value as JsonObject); }; } From 6b389258af829d0b275cc7b806262c4c9878a1cc Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 22:59:07 -0500 Subject: [PATCH 3/9] Remove the ability to fromJson() nothing, as per a PR comment --- serializable.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/serializable.ts b/serializable.ts index 2d54c455..8deac7c7 100644 --- a/serializable.ts +++ b/serializable.ts @@ -34,10 +34,9 @@ export abstract class Serializable { public toJson(): string { return toJson(this); } - public fromJson(): this; public fromJson(json: string): this; public fromJson(json: JsonObject): this; - public fromJson(json: string | JsonObject = {}): this { + public fromJson(json: string | JsonObject): this { return fromJson(this, json); } } From ad6a45f1bffae67f8d7f1432fddcacfab504787a Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:00:39 -0500 Subject: [PATCH 4/9] Remove redundant typing in test as per PR comment --- serialize_property_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serialize_property_test.ts b/serialize_property_test.ts index 99ae167a..b8fceeec 100644 --- a/serialize_property_test.ts +++ b/serialize_property_test.ts @@ -349,7 +349,7 @@ test({ class Test2 extends Serializable { @SerializeProperty({ serializedKey: "serialize_me_2", - fromJsonStrategy: (json: JsonValue) => + fromJsonStrategy: json => new Test1().fromJson(json as JsonObject), }) nested!: Test1; From 64b11a044f1218b48d0b5e54538e2b2e4592ef87 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:05:20 -0500 Subject: [PATCH 5/9] Don't require JsonObjects when JsonValues will do, as per PR comment --- from_json/as.ts | 8 +++----- serializable.ts | 10 +++++----- serialize_property_test.ts | 8 ++++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/from_json/as.ts b/from_json/as.ts index 4b1fb762..f893e336 100644 --- a/from_json/as.ts +++ b/from_json/as.ts @@ -2,8 +2,6 @@ import { FromJsonStrategy, - JsonObject, - JsonValue, Serializable, } from "../serializable.ts"; @@ -11,10 +9,10 @@ import { export function fromJsonAs( type: T & { new (): Serializable }, ): FromJsonStrategy { - return (value: JsonValue) => { + return value => { if (Array.isArray(value)) { - return value.map((item) => new type().fromJson(item as JsonObject)); + return value.map((item) => new type().fromJson(item)); } - return new type().fromJson(value as JsonObject); + return new type().fromJson(value); }; } diff --git a/serializable.ts b/serializable.ts index 8deac7c7..4332d618 100644 --- a/serializable.ts +++ b/serializable.ts @@ -35,8 +35,8 @@ export abstract class Serializable { return toJson(this); } public fromJson(json: string): this; - public fromJson(json: JsonObject): this; - public fromJson(json: string | JsonObject): this { + public fromJson(json: JsonValue): this; + public fromJson(json: string | JsonValue): this { return fromJson(this, json); } } @@ -161,16 +161,16 @@ function toJson(context: T): string { /** Convert from object/string to mapped object on the context */ function fromJson(context: Serializable, json: string): T; -function fromJson(context: Serializable, json: JsonObject): T; +function fromJson(context: Serializable, json: JsonValue): T; function fromJson( context: Serializable, - json: string | JsonObject, + json: string | JsonValue, ): T; function fromJson( context: Serializable, - json: string | JsonObject, + json: string | JsonValue, ): T { const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get( context?.constructor?.prototype, diff --git a/serialize_property_test.ts b/serialize_property_test.ts index b8fceeec..2a3a2ab3 100644 --- a/serialize_property_test.ts +++ b/serialize_property_test.ts @@ -7,7 +7,7 @@ import { fail, test, } from "./test_deps.ts"; -import { JsonObject, JsonValue, Serializable } from "./serializable.ts"; +import { Serializable } from "./serializable.ts"; import { ERROR_MESSAGE_SYMBOL_PROPERTY_NAME, SerializeProperty, @@ -216,8 +216,8 @@ test({ } class Test extends Serializable { @SerializeProperty({ - fromJsonStrategy: (v: JsonValue) => - new OtherClass().fromJson(v as JsonObject), + fromJsonStrategy: v => + new OtherClass().fromJson(v), }) array!: OtherClass[]; } @@ -350,7 +350,7 @@ test({ @SerializeProperty({ serializedKey: "serialize_me_2", fromJsonStrategy: json => - new Test1().fromJson(json as JsonObject), + new Test1().fromJson(json), }) nested!: Test1; } From 42270ffab3c4fd9e771b11f886e38f72c6225b79 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:07:10 -0500 Subject: [PATCH 6/9] Linter --- serialize_property_test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/serialize_property_test.ts b/serialize_property_test.ts index 2a3a2ab3..7bb50e16 100644 --- a/serialize_property_test.ts +++ b/serialize_property_test.ts @@ -349,8 +349,7 @@ test({ class Test2 extends Serializable { @SerializeProperty({ serializedKey: "serialize_me_2", - fromJsonStrategy: json => - new Test1().fromJson(json), + fromJsonStrategy: json => new Test1().fromJson(json), }) nested!: Test1; } From 4ee15ae29300986a6a1e02eff425625678a01caa Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:08:57 -0500 Subject: [PATCH 7/9] Linter harder --- from_json/as.ts | 5 +---- serialize_property_test.ts | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/from_json/as.ts b/from_json/as.ts index f893e336..22bb6fbb 100644 --- a/from_json/as.ts +++ b/from_json/as.ts @@ -1,9 +1,6 @@ // Copyright 2018-2020 Gamebridge.ai authors. All rights reserved. MIT license. -import { - FromJsonStrategy, - Serializable, -} from "../serializable.ts"; +import { FromJsonStrategy, Serializable } from "../serializable.ts"; /** revive data using `fromJson` on a subclass type */ export function fromJsonAs( diff --git a/serialize_property_test.ts b/serialize_property_test.ts index 7bb50e16..40cbba3e 100644 --- a/serialize_property_test.ts +++ b/serialize_property_test.ts @@ -216,8 +216,7 @@ test({ } class Test extends Serializable { @SerializeProperty({ - fromJsonStrategy: v => - new OtherClass().fromJson(v), + fromJsonStrategy: (v) => new OtherClass().fromJson(v), }) array!: OtherClass[]; } @@ -349,7 +348,7 @@ test({ class Test2 extends Serializable { @SerializeProperty({ serializedKey: "serialize_me_2", - fromJsonStrategy: json => new Test1().fromJson(json), + fromJsonStrategy: (json) => new Test1().fromJson(json), }) nested!: Test1; } From 1318e2e935191fcdb7a48b329ee54ef6bd96d6c2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:09:46 -0500 Subject: [PATCH 8/9] Parens for the linter, the linter wants parens. --- from_json/as.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/from_json/as.ts b/from_json/as.ts index 22bb6fbb..ba522a8b 100644 --- a/from_json/as.ts +++ b/from_json/as.ts @@ -6,7 +6,7 @@ import { FromJsonStrategy, Serializable } from "../serializable.ts"; export function fromJsonAs( type: T & { new (): Serializable }, ): FromJsonStrategy { - return value => { + return (value) => { if (Array.isArray(value)) { return value.map((item) => new type().fromJson(item)); } From eb9133be80db53f8491816e99cab2983385984eb Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 7 Nov 2020 23:14:36 -0500 Subject: [PATCH 9/9] Changelog update for JsonValue --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87156103..6ab3e4f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +The input to fromJson() is now a JsonValue, enforcing basic JSON requirements: +property values must be legal JSON values. This is meant to allow the +compiler to flag accidental deserializations of already-deserialized objects. ### Added -## [v0.3.0] - 2020-11-6 +## [v0.3.0] - 2020-11-06 ### Changed - updated node example to be a properly formatted node module