npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scalar/json-magic

v0.18.0

Published

A collection of utilities for working with JSON objects, including diffing, conflict resolution, bundling and more.

Readme

@scalar/json-magic

Version Downloads License Discord

A collection of utilities for working with JSON objects, including diffing, conflict resolution, bundling and more.


Scalar is an open-source API platform for teams who want beautiful developer interfaces without vendor lock-in.

  • API References — Interactive API documentation from OpenAPI and AsyncAPI specs.
  • Developer Docs — Write in Markdown/MDX, generate API references, sync with two-way Git.
  • SDK Generator — Type-safe SDKs and CLIs in TypeScript, Python, Go, PHP, Java, and Ruby.
  • API Client — Open-source, offline-first Postman alternative built on OpenAPI.

20M+ monthly npm installs · 15,500+ GitHub stars · MIT licensed · scalar.com


Installation

Requires Node.js 22.19.0 or later.

npm add @scalar/json-magic

Entry points

There is no root export. Every module is imported from its own entry point, so you only pay for what you use.

| Entry point | Exports | | --- | --- | | @scalar/json-magic/bundle | bundle, bundleWithDiagnostics, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, plus the Plugin, LoaderPlugin, LifecyclePlugin and ResolveResult types | | @scalar/json-magic/bundle/plugins/browser | fetchUrls, parseJson, parseYaml | | @scalar/json-magic/bundle/plugins/node | fetchUrls, parseJson, parseYaml, readFiles | | @scalar/json-magic/bundle/value-generator | getHash, generateUniqueValue, uniqueValueGeneratorFactory | | @scalar/json-magic/dereference | dereference | | @scalar/json-magic/join | join, plus the JoinOptions, JoinStrategy, JoinContext, JoinConflict and JoinResult types | | @scalar/json-magic/diff | diff, merge, apply, the Difference type | | @scalar/json-magic/magic-proxy | createMagicProxy, getRaw | | @scalar/json-magic/helpers/* | Small standalone helpers, see Helpers |

Which module do I need?

| I want to … | Use | | -------------------------------------------------------------------- | ----------------------------- | | Pull external $ref documents into a single self-contained document | bundle | | Read through $ref pointers without rewriting the document | magic-proxy | | Resolve every $ref, internal and external, in one call | dereference | | Compare two documents and merge concurrent edits | diff |

join

join combines JSON objects without assuming a document standard. It merges objects recursively and replaces arrays and scalar values with those from later inputs. It does not mutate inputs, upgrade document versions, resolve references, or rename definitions. Literal keys such as __proto__, constructor, and prototype are preserved as own data properties without changing object prototypes.

import { join } from '@scalar/json-magic/join'

const result = join(
  [
    {
      title: 'First',
      catalog: { apple: { price: 2 } },
      labels: [{ name: 'fruit' }],
    },
    {
      title: 'Second',
      catalog: { pear: { price: 3 } },
      labels: [{ name: 'fruit' }],
    },
  ],
  {
    strategy: ({ path }) => {
      if (path[0] === 'catalog' && path.length === 2) {
        return 'conflict'
      }
      if (path[0] === 'labels') {
        return { uniqueBy: 'name' }
      }
      return 'merge'
    },
  },
)

if (result.ok) {
  console.log(result.document) // Both catalog entries, title "Second", one fruit label
} else {
  console.log(result.conflicts) // Example: [{ path: ['catalog', 'apple'] }]
}

The optional strategy callback receives { path, current, incoming } for each visited field. Paths are arrays of literal keys, so a key containing / remains one segment. The root always merges; returning replace or conflict for an object treats that object as a whole and does not visit its children.

| Strategy | Behavior | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | merge (default) | Recursively merge objects; replace other values using the later input. | | merge-by-index | Recursively merge objects and arrays, combining array entries at matching indexes and retaining trailing entries. | | skip | Ignore this incoming field, leaving any existing value unchanged. | | replace | Replace the entire value, including objects. | | conflict | Report a duplicate key, even if both values are equal or the earlier value is null. | | { uniqueBy: 'name' } | Combine arrays, retaining the first item for each identity property value. Items without that property remain distinct. Use scalar identity values. Non-array incoming values replace the existing value. |

An empty input list returns { ok: true, document: {} }. Conflicts return { ok: false, conflicts } without a partial document. Inputs must be acyclic JSON objects.

For OpenAPI, use join from @scalar/openapi-parser, which supplies OpenAPI rules, version upgrades, and optional component prefixes. Other formats can supply their own rules; this module does not include an AsyncAPI adapter.

bundle

Preserve selected references

Pass the synchronous preserveReference(ref, { origin }) option to leave selected $ref edges authored as-is. ref includes the original fragment; origin is the effective source URI, including enclosing $id values and any resolveDocument identity. Returning true bypasses target lookup, loading, cache insertion, and reference rewriting, even for already indexed or cached targets. Sibling fields are still traversed. The callback is not used for loading the input root.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'

const result = await bundle('https://example.test/api/root.json', {
  plugins: [fetchUrls(), readFiles()],
  treeShake: true,
  preserveReference: (ref) => /^https?:\/\//i.test(ref),
})

This loads the URL root and bundles local files, internal references, and relative references such as ./child.json inside remote documents. Only authored absolute HTTP(S) references are preserved by this predicate; keep the HTTP loader enabled. Preservation emits no resolution start, success, or error hooks. Real failures on other edges retain normal error reporting. Node-processing hooks still run.

When this option is present, indexed $id values are resolved against their original effective bases so serialization at another location retains schema identity and anchor scope. Like normal bundling, this can mutate input and cached objects. URL maps include only bundled resources. Omit the option to retain the existing bundling behavior. Predicates preserving relative references are responsible for ensuring those references remain meaningful at the output base; use the absolute HTTP(S) predicate above for portable external references.

Known limitation: an embedded schema with its own $id cannot reliably reference another bundled resource through the outer document when that outer document has no declared identity. For example, a fragment-only #/x-ext/... reference inside that schema resolves against the schema, not the outer bundle. Supplying a format-appropriate root identity through resolveDocument avoids this case. Preserving selected references does not currently fix anonymous-root cross-scope bundling; $id normalization alone does not make that output portable.

bundle walks a JSON object, resolves every external $ref (URLs, local files, or anything a custom loader plugin can handle) and embeds the result into the document itself. The original $ref values are rewritten to point at the embedded copies, so the output is a single self-contained document.

Document formats can supply a resolveDocument lifecycle hook returning { baseUri, metadata }. The bundler resolves relative references against that base URI and retains the supplied root metadata when tree shaking. The hook also applies to cached and previously bundled documents. Without a hook, the retrieval URI remains the document base. JSON Schema $id values resolve against their enclosing base.

When reading qualified root references with createMagicProxy, pass the canonical URI as documentUri. Interpretation of format-specific identity fields belongs in the caller or a plugin.

URI resolution applies to every bundling path, including descriptions without $self:

  • Absolute scheme-bearing references (https:, file:, urn:, mailto:, or custom schemes) retain their identity. This does not enable fetching those schemes; loader plugins still decide what they support.
  • Hierarchical URI bases use URL resolution. Root-relative references replace the pathname; protocol-relative references replace the host. A new document path drops the base query and fragment, while query-only or fragment-only references keep the applicable parts of the base.
  • A base ending in / denotes a directory. Without the slash, the final segment is a document name. Scheme-less paths continue to use filesystem resolution, including Windows drive paths.
  • Opaque identities such as URNs support absolute and fragment references, but cannot provide a directory for a relative document path. Such a resolution throws rather than inventing a local path.
  • Relativization preserves query strings, fragments, and directory slashes. HTTP references become relative only when resolving them again reproduces the original URL. Non-HTTP URIs remain absolute: the generic helper has no document registry, so limiting this rule to known $self values would corrupt other identifiers.

These are intentional URI compatibility changes, rather than behavior limited to OpenAPI $self.

External documents are stored under the x-ext key, and the mapping between the generated keys and their original URLs is stored under x-ext-urls. Both keys are configurable, see Options.

Quick start

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

const result = await bundle(
  { $ref: 'http://example.com/document.json' },
  {
    plugins: [fetchUrls()],
    treeShake: false,
  },
)

// The bundled document
console.log(result)

When the input is an object it is modified in place, and the same object is returned. When the input is a string it is resolved with the given plugins first.

Bundling with reference diagnostics

bundleWithDiagnostics returns { document, diagnostics, complete, pruning }. It uses the same loaders, lifecycle hooks, URI registry, compression, and URL map options as bundle. Existing bundle calls retain their return value and generic traversal behavior.

import { bundleWithDiagnostics } from '@scalar/json-magic/bundle'
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'

const result = await bundleWithDiagnostics('/api/openapi.yaml', {
  plugins: [readFiles(), fetchUrls({ limit: 5 })],
  traversal: 'document',
  treeShake: true,
  urlMap: true,
})

for (const diagnostic of result.diagnostics) {
  console.error(diagnostic.source, diagnostic.pointer, diagnostic.kind, diagnostic.reference)
}

// The caller chooses whether to serialize an incomplete document.
if (result.complete || allowIncompleteOutput) {
  writeDocument(result.document)
}

Each diagnostic contains:

  • kind: unresolved-document for loading/parsing failures (including missing loaders), or missing-target for a missing pointer/anchor in a loaded document.
  • reference: the authored reference, captured before lifecycle processing or bundler rewriting.
  • source: the source URI/path, including for external documents. Object inputs use origin, or / when none is provided.
  • pointer: the escaped JSON pointer to the authored $ref field, such as #/components/schemas/Pet/properties/name/$ref.

Recoverable reference failures leave their references intact and return complete: false. A missing external fragment does not become a dangling pointer into x-ext. Unreadable or unparseable roots and unexpected bundling or lifecycle-hook failures reject the promise; they do not return an incomplete result. Custom loaders may return { ok: false } or reject to report a loading failure. All loader exceptions and rejected cache entries become unresolved-document diagnostics for external references, including programming errors inside custom loaders; the loader contract does not classify exceptions. Loading and parsing share one diagnostic kind because existing loaders do not distinguish them.

Pruning runs after reference resolution, without refetching or replaying lifecycle hooks. pruning is disabled when treeShake is false, applied after successful resolution, or skipped when diagnostics make reachability uncertain. A skipped prune retains unpruned external documents. Diagnostics cover visited source content, including unused parts of loaded documents, before pruning. Existing depth and partial-root options still limit traversal.

The diagnostics API keeps cached loader data unmodified, cloning it into each bundling run. A supplied cache can be reused across calls and can contain preloaded resources and identities. New cache entries use resolved source URIs; relative preloaded keys remain supported by the bundler's registry. Object inputs are still modified in place. Cache values must be structured-cloneable JSON/YAML data.

Set preserveReference: (reference, { origin }) => boolean to intentionally leave selected references untouched. Preserved references are not failures, produce no diagnostics, and are not fetched. complete means no resolution failures were found in the visited content; it does not promise that intentionally preserved or depth-limited references are embedded.

Document-aware traversal

traversal: 'document' is an explicit opt-in on either bundling API. It recognizes OpenAPI and AsyncAPI roots; other roots are treated as JSON Schema fragments. The default, generic, keeps treating arbitrary $ref properties as references.

Document mode leaves instance data untouched in examples, defaults, enum/const values, Example Object values, and extensions. Actual references to Example Objects, callbacks, schemas, and AsyncAPI messages/traits remain active. Message and message-trait examples are literal data. Schema property names are interpreted as names, even when they are examples, default, or start with x-. External fragments inherit the context of their referring object.

Applicable schema and Path Item siblings are traversed internally. OpenAPI 2.0/3.0 Reference Object siblings and explicitly declared older JSON Schema draft siblings remain ignored. Siblings are preserved separately, not merged; this API does not decide conflicts between Path Item fields. Recursive schemas and reference-only cycles are allowed when their targets exist. $id, $anchor, custom resolveDocument identities, escaped pointers, and $global chunk context use the existing registry.

This is reference resolution, not OpenAPI or AsyncAPI validation. It does not validate required fields, target object types, unknown schema vocabularies, or schema dialect conformance. It does not add document metadata, output policy, or CLI flags.

Replacing a CLI reference checker

Before, a CLI had to inspect sources, temporarily mask literal $ref values, supply cached loaders, and recursively invoke the bundler from sibling hooks:

const inspected = await inspectReferences(input, loaders)
const document = await bundle(inspected.document, {
  plugins: [inspected.loader],
  treeShake: prune && inspected.diagnostics.length === 0,
  hooks: recursiveSiblingHooks,
})
inspected.restore(document)

After, the library performs those steps in one operation:

const { document, diagnostics, complete, pruning } = await bundleWithDiagnostics(input, {
  plugins: loaders,
  traversal: 'document',
  treeShake: prune,
  urlMap,
})
// The CLI retains its own failure reporting, force flag, and filesystem output policy.
if (!complete && !force) throw new Error(formatDiagnostics(diagnostics))
await writeOutput(document)

No separate checker, Symbol masking, or recursive sibling hooks are needed. Hooks that implement unrelated features and custom document identity can remain.

Loaders

A loader plugin teaches the bundler how to read one kind of reference. Import loaders from @scalar/json-magic/bundle/plugins/browser in the browser, or from @scalar/json-magic/bundle/plugins/node in Node.js. The Node entry point is a superset: it adds readFiles, which needs filesystem access.

| Loader | Handles | Available in | | ----------- | ------------------------------------- | ------------- | | fetchUrls | http:// and https:// references | browser, node | | readFiles | Local file paths | node | | parseJson | A raw JSON string passed as the input | browser, node | | parseYaml | A raw YAML string passed as the input | browser, node |

You can combine as many loaders as you need. The first one whose validate returns true wins.

fetchUrls

Resolves remote documents over HTTP. It works in both Node.js and the browser.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

const document = {
  openapi: '3.1.0',
  info: { title: 'Bundled API', version: '1.0.0' },
  paths: {},
  components: {
    schemas: {
      User: { $ref: 'https://example.com/user-schema.json#' },
    },
  },
}

// This bundles all external documents and turns external references into internal ones
await bundle(document, {
  plugins: [fetchUrls()],
  // Removes the parts of the external documents that are not referenced
  treeShake: true,
})

console.log(document)
Limiting the number of concurrent requests
import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(document, {
  plugins: [
    fetchUrls({
      // Run at most 10 requests at the same time
      limit: 10,
    }),
  ],
  treeShake: false,
})
Custom headers

Headers are matched against the host of the reference, so a token is only ever sent to the domains you list.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(document, {
  plugins: [
    fetchUrls({
      headers: [
        {
          domains: ['example.com'],
          headers: {
            Authorization: 'Bearer <TOKEN>',
          },
        },
      ],
    }),
  ],
  treeShake: false,
})
Custom fetch function

For advanced use cases like proxying requests or implementing custom network logic, you can provide your own fetch implementation. This allows you to handle things like CORS restrictions, custom authentication flows, or request and response transformations.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(document, {
  plugins: [
    fetchUrls({
      fetch: async (input, init) => {
        console.log('Custom fetch logic')
        return fetch(input, init)
      },
    }),
  ],
  treeShake: false,
})
Bundle from a remote URL

The input itself can be a URL, as long as a loader can handle it.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

const result = await bundle('https://example.com/openapi.json', {
  plugins: [fetchUrls()],
  treeShake: false,
})

// The bundled document
console.log(result)

readFiles

Resolves local files. This loader only works in a Node.js environment.

import { bundle } from '@scalar/json-magic/bundle'
import { readFiles } from '@scalar/json-magic/bundle/plugins/node'

const document = {
  openapi: '3.1.0',
  info: { title: 'Bundled API', version: '1.0.0' },
  paths: {},
  components: {
    schemas: {
      User: { $ref: './user-schema.json#' },
    },
  },
}

await bundle(document, {
  plugins: [readFiles()],
  treeShake: false,
})

console.log(document)
Bundle from a local file

You can pass the file path directly, as long as readFiles is registered to read it.

import { bundle } from '@scalar/json-magic/bundle'
import { readFiles } from '@scalar/json-magic/bundle/plugins/node'

const result = await bundle('./input.json', {
  plugins: [readFiles()],
  treeShake: false,
})

// The bundled document
console.log(result)

A document that mixes remote and local references needs both loaders:

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'

await bundle('./openapi.json', {
  plugins: [readFiles(), fetchUrls()],
  treeShake: false,
})

parseJson

Accepts a raw JSON string as the input.

import { bundle } from '@scalar/json-magic/bundle'
import { parseJson } from '@scalar/json-magic/bundle/plugins/browser'

const result = await bundle('{ "openapi": "3.1.1" }', {
  plugins: [parseJson()],
  treeShake: false,
})

// The bundled document
console.log(result)

parseYaml

Accepts a raw YAML string as the input.

import { bundle } from '@scalar/json-magic/bundle'
import { parseYaml } from '@scalar/json-magic/bundle/plugins/browser'

const result = await bundle('openapi: "3.1.1"\n', {
  plugins: [parseYaml()],
  treeShake: false,
})

// The bundled document
console.log(result)

Writing a custom loader

A loader is a plain object, so you can resolve references from anywhere: a database, an in-memory map, or a custom protocol.

import { bundle, type LoaderPlugin } from '@scalar/json-magic/bundle'

const workspaceFiles: LoaderPlugin = {
  type: 'loader',
  validate: (value) => value.startsWith('workspace:'),
  exec: async (value) => {
    const raw = await readFromWorkspace(value.replace('workspace:', ''))

    if (raw === undefined) {
      return { ok: false }
    }

    return { ok: true, data: JSON.parse(raw), raw }
  },
}

await bundle(document, {
  plugins: [workspaceFiles],
  treeShake: false,
})

exec returns { ok: true, data, raw } on success and { ok: false } on failure. A failed resolution is reported through the onResolveError hook and logged as a warning, it does not throw.

Options

| Option | Type | Default | Description | | ------------------------------ | ---------------------------------------------- | -------------- | --------------------------------------------------------------------------------------- | | plugins | Plugin[] | — | Loader and lifecycle plugins used during bundling. Required. | | treeShake | boolean | — | Only keep the parts of external documents that are actually referenced. Required. | | depth | number | unlimited | How deeply nested $ref pointers are followed. See the note below. | | root | UnknownObject | the input | Base document to write external documents into, used for partial bundling. | | origin | string | the input | Base path used to resolve relative references. | | cache | Map<string, Promise<ResolveResult>> | new map | Cache of in-flight resolutions, reused across bundle calls to avoid refetching. | | visitedNodes | Set<unknown> | new set | Nodes that have already been bundled, used to avoid re-bundling during partial bundles. | | urlMap | boolean | false | Keep the mapping of generated keys to their original URLs in the output. | | externalDocumentsKey | string | 'x-ext' | Key that holds the bundled external documents. | | externalDocumentsMappingsKey | string | 'x-ext-urls' | Key that holds the mapping between generated keys and original URLs. | | compress | (value: string) => string \| Promise<string> | hash | Function used to shorten URLs and file paths into document keys. | | hooks | object | — | Lifecycle hooks, see Hooks. |

depth

The depth option controls how deeply the bundler resolves $ref references. When you set depth to a number, the bundler only follows references up to that level of nesting. This is useful for creating partial bundles or limiting resource usage.

Note: When using depth, the resulting bundle may not be fully self-contained, since nested references deeper than the given depth stay unresolved. If you use depth together with visitedNodes, be aware that parent nodes may be marked as visited even if their child references have not been fully resolved yet. Use this option with care if you require a complete bundle.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(input, {
  plugins: [fetchUrls()],
  treeShake: false,
  depth: 2,
})

externalDocumentsKey

The externalDocumentsKey option controls the key used to store external references. This key contains all bundled external documents, which keeps a clean separation between the main document and its bundled references. Defaults to x-ext.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(input, {
  plugins: [fetchUrls()],
  treeShake: false,
  externalDocumentsKey: 'x-external-docs',
})

externalDocumentsMappingsKey

The externalDocumentsMappingsKey option controls the key used to maintain a mapping between generated keys and their original URLs. This mapping is essential for tracking the source of bundled references. It is removed from the output of a full bundle unless urlMap is enabled. Defaults to x-ext-urls.

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

await bundle(input, {
  plugins: [fetchUrls()],
  treeShake: false,
  urlMap: true,
  externalDocumentsMappingsKey: 'x-external-urls',
})

Hooks

Hooks let you observe and extend the bundling process, which is handy for progress reporting and error tracking.

| Hook | Called when | | --------------------- | ---------------------------------------- | | onResolveStart | The bundler starts resolving a $ref | | onResolveSuccess | A $ref was resolved successfully | | onResolveError | A $ref could not be resolved | | onBeforeNodeProcess | Before a node is processed, may be async | | onAfterNodeProcess | After a node is processed, may be async |

import { bundle } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

const errors: string[] = []

await bundle(document, {
  plugins: [fetchUrls()],
  treeShake: true,
  hooks: {
    onResolveStart: (node) => console.log('Resolving:', node.$ref),
    onResolveSuccess: (node) => console.log('Resolved:', node.$ref),
    onResolveError: (node) => errors.push(`Failed to resolve ${node.$ref}`),
  },
})

The same hooks can be shipped as a reusable lifecycle plugin, which is then passed alongside the loaders:

import { bundle, type LifecyclePlugin } from '@scalar/json-magic/bundle'
import { fetchUrls } from '@scalar/json-magic/bundle/plugins/browser'

const logProgress: LifecyclePlugin = {
  type: 'lifecycle',
  onResolveSuccess: (node) => console.log('Resolved:', node.$ref),
}

await bundle(document, {
  plugins: [fetchUrls(), logProgress],
  treeShake: false,
})

dereference

dereference resolves the $ref pointers of a document and returns the result wrapped in a magic proxy, so referenced values are read through the $ref-value property.

It works in two modes:

  • Synchronous (sync: true): only internal references are resolved, no network requests are made, and the result is returned directly.
  • Asynchronous (sync: false, the default): external references are bundled into the document first, so both internal and external references resolve. A Promise is returned.

Options

| Option | Type | Default | Description | | --------- | ---------- | --------------- | -------------------------------------------------------------- | | sync | boolean | false | Resolve internal references only, without returning a Promise. | | plugins | Plugin[] | [fetchUrls()] | Loaders used to resolve external references in async mode. |

The result is an object with a success property. On success it carries the dereferenced data, and on failure it carries an errors array describing the references that could not be resolved.

import { dereference } from '@scalar/json-magic/dereference'

const result = dereference({ a: 'hello', b: { $ref: '#/a' } }, { sync: true })

if (result.success) {
  // 'hello'
  console.log(result.data.b['$ref-value'])
}

To resolve external references as well, drop sync (or set it to false) and await the result.

import { dereference } from '@scalar/json-magic/dereference'

const result = await dereference({
  a: 'hello',
  b: { $ref: 'http://example.com/document.json#/somepath' },
})

if (result.success) {
  console.log(result.data)
} else {
  console.error(result.errors)
}

To resolve references from somewhere other than HTTP, pass your own loaders:

import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node'
import { dereference } from '@scalar/json-magic/dereference'

const result = await dereference(document, {
  plugins: [readFiles(), fetchUrls()],
})

diff

Compare two JSON objects, merge changes made in parallel, and surface the conflicts that need to be resolved manually.

| Function | Description | | --------------------- | ---------------------------------------------------------------------------------------- | | diff(base, updated) | Returns the list of add, update and delete changes that turn base into updated | | merge(diffA, diffB) | Combines two changesets made against the same base into { diffs, conflicts } | | apply(base, diffs) | Applies a changeset to a document and returns it |

These three functions work on the documents themselves, not on copies of them. apply mutates the document it is given, merge merges one changeset into the entries of the other, and the changes a diff carries are live references into the documents that were compared. That means an applied document keeps sharing subtrees with the document it was diffed against, and a merge writes into the document behind its second changeset. Deep clone the documents before diffing them whenever you need the originals to stay untouched — cloning only the document you apply to is not enough.

apply throws an InvalidChangesDetectedError when a change points at a path that does not exist, when a change targets the document itself (an empty path, which a diff produces whenever the two documents differ at the root), and when a path reaches the prototype chain through __proto__, constructor or prototype.

Quickstart

import { apply, diff, merge } from '@scalar/json-magic/diff'

const baseObject = {
  openapi: '3.0.0',
  info: {
    title: 'Simple API',
    description: 'A small OpenAPI specification example',
    version: '1.0.0',
  },
}

const objectV1 = {
  openapi: '3.0.0',
  info: {
    title: 'Simple API',
    description: 'A small OpenAPI specification example',
    version: '1.0.0',
  },
  change: 'This is a new property',
}

const objectV2 = {
  openapi: '3.0.0',
  info: {
    title: 'Simple API',
    description: 'A small OpenAPI specification example',
    version: '1.0.1',
  },
}

// Merge the changes of both versions with the same parent object
const { diffs, conflicts } = merge(
  diff(baseObject, objectV1),
  diff(baseObject, objectV2),
)

// Apply changes from v1 and v2 to the parent object to get the final object
const finalDocument = apply(baseObject, diffs)

Conflicts

merge only returns the changes it can combine safely. When both sides touch the same path, the pair is returned in conflicts for you to resolve, and it is left out of diffs.

import { diff, merge } from '@scalar/json-magic/diff'

const { diffs, conflicts } = merge(diff(base, mine), diff(base, theirs))

for (const [mineChanges, theirChanges] of conflicts) {
  // Decide which side wins, then push the winner onto diffs
  diffs.push(...mineChanges)
}

magic-proxy

A JavaScript proxy that resolves internal references as you access properties. Nothing is copied or rewritten: the underlying document keeps its $ref pointers, and referenced values are read on demand through the virtual $ref-value property. Resolved values are cached, and proxies are stable for the same target object, which makes it safe to use with reactive frameworks like Vue.

Quick start

import { createMagicProxy, getRaw } from '@scalar/json-magic/magic-proxy'

const result = createMagicProxy({
  a: 'hello',
  b: {
    $ref: '#/a',
  },
})

// 'hello', resolved on access
console.log(result.b['$ref-value'])

// '#/a', the original pointer is still there
console.log(result.b.$ref)

/**
 * getRaw returns the untouched object:
 * {
 *   a: 'hello',
 *   b: {
 *     $ref: '#/a'
 *   }
 * }
 */
const rawObject = getRaw(result)
console.log(rawObject)

Deeply nested values are proxied too, so $ref-value works at any depth. Writes pass through to the underlying object, and writing to $ref-value updates the value the reference points at.

Properties prefixed with __scalar_ are treated as internal: they read as undefined, they are left out of Object.keys, and in checks return false. Pass { showInternal: true } to expose them.

const proxy = createMagicProxy(
  { __scalar_meta: 'hidden' },
  { showInternal: true },
)

// 'hidden'
console.log(proxy.__scalar_meta)

Helpers

Standalone helpers, each with its own entry point.

| Helper | Description | | --------------------------------------------------- | ------------------------------------------------------- | | @scalar/json-magic/helpers/escape-json-pointer | Escape ~ and / in a JSON pointer segment | | @scalar/json-magic/helpers/unescape-json-pointer | Reverse of escapeJsonPointer | | @scalar/json-magic/helpers/get-segments-from-path | Split a JSON pointer into unescaped segments | | @scalar/json-magic/helpers/get-value-by-path | Read the value at a list of path segments | | @scalar/json-magic/helpers/set-value-at-path | Write a value at a JSON pointer, creating missing nodes | | @scalar/json-magic/helpers/is-file-path | Check whether a string looks like a file path | | @scalar/json-magic/helpers/is-http-url | Check whether a string is an HTTP or HTTPS URL | | @scalar/json-magic/helpers/is-json-object | Check whether a string is a JSON object | | @scalar/json-magic/helpers/is-yaml | Check whether a string is valid YAML | | @scalar/json-magic/helpers/normalize | Parse a JSON or YAML string into an object |

import { getValueByPath } from '@scalar/json-magic/helpers/get-value-by-path'

const { value } = getValueByPath(
  { components: { schemas: { User: { type: 'object' } } } },
  ['components', 'schemas', 'User'],
)

Community

We are API nerds. You too? Let's chat on Discord: https://discord.gg/scalar

License

The source code in this repository is licensed under MIT.