CodableJSON

repository·main·Indexed 41 days ago

https://github.com/pie6k/codablejson

A 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.

Tokens
23.8K
Snippets
73
Records
86
Agent score
40%

What's inside CodableJSON

  1. Automatic discovery of complex dependency graphs

    main

    The Coder instance automatically traverses the dependency tree. By registering only the root class in the Coder constructor, 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 discovered
  2. Understand codablejson reference preservation modes

    main

    When 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.
  3. How CodableJSON handles format escaping

    main

    CodableJSON 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"
  4. Handle format conflicts with automatic escaping

    main

    If 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 restored
  5. Use external references for non-serializable dependencies

    main

    External 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 @external property, codablejson does 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 externalReferences option of the coder.decode method. If a required external reference is missing from this map, decode will 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 === context
  6. How inheritance works with declarative serialization

    main

    CodableJSON 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 dependencies option in @codableClass to declare parent classes. While not strictly required for the inheritance to function, declaring dependencies allows 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., Grandchild extends Child extends Parent).
  7. Understand reference behavior in copy()

    main

    The copy function 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)
  8. How CodableJSON protects against prototype pollution

    main

    CodableJSON includes built-in security measures to prevent prototype pollution attacks. It achieves this through two primary mechanisms:

    1. Filtering during serialization: When using encode(), CodableJSON automatically identifies and removes dangerous properties from the object before they are encoded.
    2. Safe deserialization: During decode(), CodableJSON creates objects in a way that prevents modification of the global Object.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
  9. Follow the memberwise constructor pattern for automatic class handling

    main

    To 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 Codable structs.

    If your class uses a custom constructor (e.g., positional arguments), the @codableClass TypeScript definition will flag an error unless you provide an encode function 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;
      }
    }
  10. How object references are preserved during serialization

    main

    CodableJSON 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 $$ref key 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]; // true
  11. The `$` convention for observable properties

    main

    By 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";