Skip to content

Improve serialization/deserialization over API boundary #33

@Floriferous

Description

@Floriferous

I'm trying to send better-result types from the backend into our frontend via TRPC, and writing the serializer is not as trivial as I would hope.

The isSerializedResult does not feel specific enough for me:

function isSerializedResult(obj: unknown): obj is SerializedResult<unknown, unknown> {
  return (
    obj !== null &&
    typeof obj === "object" &&
    "status" in obj &&
    ((obj.status === "ok" && "value" in obj) || (obj.status === "error" && "error" in obj))
  );
}

Sending status: 'ok' over our non-better-result API routes seems like it could easily happen, and we would then have a false positive deserialization.

Here's what my TRPC transformer looks like for now, only transforming from backend to frontend:

import { Err, Ok, Result } from 'better-result';

const trpcTransformer = {
  input: {
    deserialize: (obj: unknown): unknown => obj,
    serialize: (obj: unknown): unknown => obj,
  },
  output: {
    deserialize: (obj: unknown): unknown => {
      // Result.serialize outputs { status: 'ok'|'error', value/error: ... }
      if (
        typeof obj === 'object' &&
        obj !== null &&
        'status' in obj &&
        ((obj as { status: string }).status === 'ok' ||
          (obj as { status: string }).status === 'error')
      ) {
        return Result.deserialize(obj);
      }
      return obj;
    },
    serialize: (obj: unknown): unknown => {
      if (obj instanceof Ok || obj instanceof Err) {
        return Result.serialize(obj as Result<unknown, unknown>);
      }
      return obj;
    },
  },
};

I'd like to have something close to this:

import { Err, Ok, Result } from 'better-result';

const trpcTransformer = {
  input: {
    deserialize: (obj: unknown): unknown => obj,
    serialize: (obj: unknown): unknown => obj,
  },
  output: {
    deserialize: (obj: unknown): unknown => {
      if (isSerializedResult(obj)) {
        return Result.deserialize(obj);
      }
      return obj;
    },
    serialize: (obj: unknown): unknown => {
      if (isResult(obj)) {
        return Result.serialize(obj as Result<unknown, unknown>);
      }
      return obj;
    },
  },
};

This leads to 2 ideas:

  • Expose isSerializedResult and assertIsResult (or similar)
  • Potentially make these checks more robust, with a more unique pattern than 'status' in value?
    • The simpler solution here is to convert the entire codebase to only send Result types from the backend, but that's not always an option

Maybe there's another alternative to get this right that I can't think of though!

Great library 👌

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions