CodableJSON
repository·main·Indexed 41 days ago
https://github.com/pie6k/codablejsonA high-performance, declarative JSON serialization library for TypeScript/JavaScript. It serves as a faster alternative to SuperJSON, supporting complex types like Date, Map, Set, BigInt, and the Temporal API. It provides decorators such as @codable, @codableClass, and @external to preserve class types, handle circular dependencies, and customize property mapping during serialization and deserialization.
What's inside CodableJSON
- CodableJSON is designed to deliver superior performance compared to alternatives (such as SuperJSON) while maintaining a developer-friendly API. It is optimized for handling both plain JSON data and complex data structures efficiently.
Automatic discovery of complex dependency graphs
mainThe
Coderinstance automatically traverses the dependency tree. By registering only the root class in theCoderconstructor, all nested dependencies—no matter how deep or complex the hierarchy—will be automatically discovered and registered for serialization.@codableClass("Comment", { dependencies: () => [User] }) class Comment { @codable() text: string; @codable() author: User; constructor(data: Memberwise<Comment, "text" | "author">) { this.text = data.text; this.author = data.author; } } @codableClass("Post", { dependencies: () => [User, Comment] }) class Post { @codable() title: string; @codable() content: string; @codable() author: User; @codable() comments: Comment[] = []; constructor(data: Memberwise<Post, "title" | "content" | "author">) { this.title = data.title; this.content = data.content; this.author = data.author; } } @codableClass("Blog", { dependencies: () => [Post] }) class Blog { @codable() name: string; @codable() posts: Post[] = []; constructor(data: Memberwise<Blog, "name">) { this.name = data.name; } } // Only need to register the root class const coder = new Coder([Blog]); // User, Comment, and Post are all automatically discoveredUnderstand codablejson reference preservation modes
mainWhen encoding data with
codablejson, you can choose how object references are handled. This affects both performance and the structure of the decoded data:- Preserve refs: The encoder analyzes the data to detect if the same object is seen multiple times. During decoding, the library will re-use these object references, maintaining the original identity of the objects.
- Copy refs: The encoder does not preserve object references. During decoding, the data will contain copies of the same object, even if they were the same reference in the original input. This mode is generally faster for encoding.
How CodableJSON handles format escaping
mainCodableJSON uses a specific internal format for encoding various types, such as
{ $$Set: [1, 2, 3] }. If your raw data contains keys that match these internal patterns (e.g.,$$Set,$$Map,$$Date), CodableJSON automatically escapes them to prevent them from being misinterpreted as custom type instructions.Escaped keys are prefixed with a tilde (
~), such as~$$Set. When the data is decoded, these keys are automatically unescaped back to their original form (e.g.,$$Set) and are treated as standard data rather than special CodableJSON type instructions.import { encode, decode } from "codablejson"; // This data conflicts with Set encoding format const conflictingData = { $$Set: [1, 2, 3], // Conflicts with Set serialization $$Map: [["key", "value"]], // Conflicts with Map serialization $$Date: "not-a-date" // Conflicts with Date serialization }; const encoded = encode(conflictingData); // { "~$$Set": [1, 2, 3], "~$$Map": [["key", "value"]], "~$$Date": "not-a-date" } const decoded = decode(encoded); // decoded.$$Set === [1, 2, 3] // decoded.$$Map === [["key", "value"]] // decoded.$$Date === "not-a-date"Handle format conflicts with automatic escaping
mainIf your data contains keys that conflict with CodableJSON's internal encoding format (keys starting with
$$), CodableJSON automatically escapes them by prefixing the key with a tilde (~). This ensures that the data is correctly restored during decoding without being mistaken for an encoded type.const data = { $$Set: [1, 2, 3] }; // This would conflict with Set encoding encode(data); // { "~$$Set": [1, 2, 3] } - automatically escaped const decoded = decode(encoded); // decoded.$$Set === [1, 2, 3] - correctly restoredUse external references for non-serializable dependencies
mainExternal references allow you to link objects that exist outside your serialized data (like SDKs, authentication providers, or context objects) to your classes without attempting to serialize them.
When you encode a class with an
@externalproperty,codablejsondoes not include the object itself in the output. Instead, it includes a reference marker:{ $$external: { key: "<name>", isOptional: <boolean> } }.To restore these references during decoding, you must provide the actual objects in the
externalReferencesoption of thecoder.decodemethod. If a required external reference is missing from this map,decodewill throw an error.import { external, codableClass, codable, Coder } from "codablejson"; class GameContext { constructor(public gameId: string, public serverUrl: string, public playerCount: number) {} } @codableClass("GameState") class GameState { @external("gameContext") context!: GameContext; @codable() players: Set<string> = new Set(); @codable() score: number = 0; constructor(data: Pick<GameState, "players" | "score" | "context">) { this.players = data.players; this.score = data.score; this.context = data.context; } } const coder = new Coder([GameState]); const context = new GameContext("game-123", "https://api.example.com", 4); const gameState = new GameState({ players: new Set(["player1", "player2"]), score: 100, context }); // Encoding only includes the reference marker const encoded = coder.encode(gameState); // Decoding requires passing the reference in the externalReferences map const decoded = coder.decode<GameState>(encoded, { externalReferences: { gameContext: context } }); // decoded.context === contextHow inheritance works with declarative serialization
mainCodableJSON supports class inheritance hierarchies. When using
@codableClass, the library ensures that properties from all levels of the hierarchy are correctly handled during encoding and decoding.Key behaviors:
- Selective Encoding: Only properties explicitly marked with the
@codable()decorator are included in the encoded JSON output, regardless of which level of the hierarchy they belong to. - Dependency Management: You can use the
dependenciesoption in@codableClassto declare parent classes. While not strictly required for the inheritance to function, declaringdependenciesallows each class to manage its own requirements, avoiding the need for a single central registry of all custom types. - Multi-Level Support: Inheritance works across multiple levels (e.g.,
GrandchildextendsChildextendsParent).
- Selective Encoding: Only properties explicitly marked with the
Understand reference behavior in copy()
mainThe
copyfunction preserves reference equality inside the new structure. While the copy itself is a new object independent of the original, if multiple elements in the original object pointed to the same reference, those elements will still point to a single (newly created) shared reference within the copy.Specifically:
- The copy is not the same reference as the original.
- Elements that were identical references in the original will be identical references in the copy.
- The elements in the copy are new references that do not point back to the original objects.
const shared = { value: "shared" }; const original = [shared, shared]; const copied = copy(original); // copied[0] !== copied[1] (new references) // copied[0] === copied[1] (reference equality is preserved inside the copy) // original[0] !== copied[0] (reference equality is preserved **inside** the copy)How CodableJSON protects against prototype pollution
mainCodableJSON includes built-in security measures to prevent prototype pollution attacks. It achieves this through two primary mechanisms:
- Filtering during serialization: When using
encode(), CodableJSON automatically identifies and removes dangerous properties from the object before they are encoded. - Safe deserialization: During
decode(), CodableJSON creates objects in a way that prevents modification of the globalObject.prototype, even if the input data contains malicious keys.
This protection applies to both plain objects and custom types registered via
codableType.import { encode, decode } from "codablejson"; // Attempted prototype pollution attack const maliciousData = { constructor: {}, __proto__: { malicious: true }, prototype: { malicious: true } }; const encoded = encode(maliciousData); // Result: {} - dangerous properties are filtered out const decoded = decode(encoded); // Result: decoded is safe - no prototype pollution occurred- Filtering during serialization: When using
Follow the memberwise constructor pattern for automatic class handling
mainTo allow CodableJSON to automatically handle your classes without extra configuration, your class should either have no constructor or a memberwise constructor.
A memberwise constructor is a constructor that takes exactly one argument: an object containing properties that match the structure of your class properties. This pattern is similar to Swift's
Codablestructs.If your class uses a custom constructor (e.g., positional arguments), the
@codableClassTypeScript definition will flag an error unless you provide anencodefunction in the decorator options to map the instance back to the constructor's expected arguments.// ✅ Memberwise constructor pattern class MemberwiseClass { foo: string; bar: number; constructor(input: Pick<MemberwiseClass, "foo" | "bar">) { this.foo = input.foo; this.bar = input.bar; } } // ❌ Non-memberwise constructor (requires manual 'encode' mapping) class NotMemberwiseClass { foo: string; bar: number; constructor(foo: string, bar: number) { this.foo = foo; this.bar = bar; } }How object references are preserved during serialization
mainCodableJSON automatically detects if the same object appears multiple times in your input (including circular references) and preserves these relationships using a reference syntax. When decoding the data, all referential equalities are maintained within the new object graph. References are represented in the encoded JSON using the
$$refkey with a path string (e.g.,"/0"for the first element in an array or"/"for the root object).import { encode, decode } from "codablejson"; const shared = { value: "shared" }; const original = [shared, shared]; const encoded = encode(original); // [{ value: "shared" }, { $$ref: "/0" }] const decoded = decode(encoded); // decoded[0] === decoded[1]; // trueThe `$` convention for observable properties
mainBy convention, developers often use a
$prefix for composed decorators (like$codable) to clearly indicate that a property is MobX-observable. This distinguishes reactive properties from standard codable properties.// Regular codable (not observable) @codable() accessor regularProperty: string = "value"; // Observable codable (MobX + CodableJSON) @$codable() accessor reactiveProperty: string = "value";