Skip to content

feat: pretty-print and truncate JSON snapshots and type shapes in error messages#2291

Open
hrastnik wants to merge 1 commit into
mobxjs:masterfrom
hrastnik:feat/pretty-print-error-snapshots
Open

feat: pretty-print and truncate JSON snapshots and type shapes in error messages#2291
hrastnik wants to merge 1 commit into
mobxjs:masterfrom
hrastnik:feat/pretty-print-error-snapshots

Conversation

@hrastnik

@hrastnik hrastnik commented Jun 1, 2026

Copy link
Copy Markdown

What does this PR do and why?

If this pull request is approved, it will make type-checking error messages far
easier to read when a large snapshot fails to convert.

Today, a failed conversion serializes the offending snapshot as a single
unbroken JSON.stringify line, and renders the target type shape
(type.describe()) as one long line too. For real-world snapshots (arrays of
objects, long strings) this produces an unreadable wall of text.

This PR improves the formatting in two places, both in
src/core/type/type-checker.ts:

  • prettyPrintValue — small values are still printed compactly on a single
    line (unchanged), but large values are now pretty-printed across multiple
    lines and truncated: long strings are clipped, big arrays/objects are capped
    with a … N more items / … N more keys marker, and deep nesting is
    summarized. This keeps errors from flooding the screen.
  • prettyPrintDescription — long model type shapes (the
    expected ... a snapshot like + "..." + part) are re-indented across lines, while union (|) and array ([]`) parts stay inline. It is written
    defensively: it never throws (it runs while an error is already being
    formatted) and falls back to the original string on any unexpected input.

Before

[mobx-state-tree] Error while converting `{"allergies":[{"id":"8f76...","title":"Egg",...}, ...600 more chars...]}` to `(Config | undefined)`: snapshot `...same huge line again...` is not assignable to type: `(Config | undefined)`, expected an instance of `(Config | undefined)` or a snapshot like `({ maintenance_mode: (boolean | undefined?); min_password_length: number; max_upload_size: number; app_versions: { ios_min: (number | null?); ... } | undefined?)` instead.

After

[mobx-state-tree] Error while converting `{
  "allergies": [
    { "id": "8f76...", "title": "Egg", ... },
    "… 17 more items"
  ],
  "note": "xxxx…… (200 more characters)"
}` to `(Config | undefined)`:

    ... expected ... a snapshot like `({
  maintenance_mode: (boolean | undefined?);
  min_password_length: number;
  max_upload_size: number;
  app_versions: {
    ios_min: (number | null?);
    ...
  }
} | undefined?)` instead.
    at path "/min_password_length" value `undefined` is not assignable to type: `number` (Value is not a number).

Small values and short type shapes are unchanged, so existing behavior (and the
existing assertions in the test suite) are preserved.

Steps to validate locally

  • bun test — full suite passes (dev), bun run test:prod passes (prod).
  • New tests in __tests__/core/type-system.test.ts cover:
    • small snapshots stay compact (regression guard)
    • large snapshots are pretty-printed across lines
    • long strings / large arrays inside snapshots are truncated
    • long type shapes are re-indented; short ones stay inline
    • a prettyPrintDescription stability block that feeds adversarial input
      (empty, over-closed/never-closed braces, separators inside string literals,
      trailing backslashes, multi-byte chars, deep nesting) and asserts it never
      throws and never loses a non-whitespace character.

…or messages

Type-checking errors previously serialized the offending snapshot as a single
unbroken JSON.stringify line and the target type shape (describe()) as one long
line, which is hard to read for large snapshots.

- prettyPrintValue: keep small values compact/inline (unchanged), but for large
  values pretty-print across lines and truncate long strings, big arrays/objects
  and deep nesting so they don't flood the screen.
- prettyPrintDescription: re-indent long model type shapes across lines, while
  leaving union/array parts inline; defensively falls back to the original
  string so it can never throw while an error is already being formatted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coolsoftwaretyler

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @hrastnik! Just wanted to let you know I've seen this and will try to get to it soon. I've got a busy schedule with some personal stuff but I am on board with the idea. Will just have to review once I find time.

@coolsoftwaretyler

Copy link
Copy Markdown
Collaborator

Hey @hrastnik - I like this idea. However, I don't like the idea of hardcoding a preferred format into the codebase itself. What if we allowed for an optional format function in our error checkers, and provided an API for consumers to pass in their preferred formatting (and maybe offer a couple of useful defaults).

That way we can make this change strictly opt-in, and flexible for people who want to do something different from these settings.

How do you feel about that change?

@coolsoftwaretyler coolsoftwaretyler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See prior comment - let's make this solution a bit more flexible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants