Skip to content

An API to resolve a nested schema using a JSON path? #1254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
aleclarson opened this issue May 20, 2025 · 2 comments
Open

An API to resolve a nested schema using a JSON path? #1254

aleclarson opened this issue May 20, 2025 · 2 comments

Comments

@aleclarson
Copy link
Contributor

https://goessner.net/articles/JsonPath/

Not sure whether this is out of scope, but JSONPath feels like a good fit with JSON Schema.

Essentially, it'd be great to have a utility that takes a TypeBox schema and a string representing a JSON path.

const schema = Type.Object({
  array: Type.Array(
    Type.Object({
      string: Type.String(),
    })
  ),
})

Type.Resolve(schema, "$.array[*].string")
// => equivalent to `Type.String()`
@aleclarson
Copy link
Contributor Author

aleclarson commented May 20, 2025

Here's my initial implementation:

import { KindGuard, TSchema } from "@sinclair/typebox";
import * as Type from "@sinclair/typebox/type";

export type JSONPath = `$${string}`;

/**
 * Given a TypeBox schema and a JSON path, return the TypeBox schema that
 * corresponds to the JSON path.
 */
export function Resolve(
  schema: TSchema,
  path: JSONPath,
  state = parseJSONPath(path),
) {
  const part = state.parts[state.index++];
  if (!part) {
    return schema;
  }
  if (part === "$") {
    return Resolve(schema, path, state);
  }
  let key: string | undefined;
  if (part.charCodeAt(0) === 46 /* . */) {
    key = part.slice(1);
  } else if (part.charCodeAt(0) === 91 /* [ */) {
    if (part.charCodeAt(1) === 39 /* ' */) {
      key = part.slice(2, -2);
    }
    // If no single quote is present, then this is either [*] or a specific
    // array index, like [0] or [1].
    else if (KindGuard.IsArray(schema)) {
      return Resolve(schema.items, path, state);
    }
  }
  if (key != null && KindGuard.IsObject(schema) && key in schema.properties) {
    return resolveNestedSchema(schema.properties[key], path, state);
  }
  return Type.Never();
}

function parseJSONPath(path: JSONPath) {
  return {
    parts: path.match(/(^\$|\.[^.[\]*]+|\[(?:\*|\d+|'[^']+')\])/gi) ?? [],
    index: 0,
  };
}

Incomplete and untested 😃

@sinclairzx81
Copy link
Owner

sinclairzx81 commented May 21, 2025

@aleclarson Hi!

Essentially, it'd be great to have a utility that takes a TypeBox schema and a string representing a JSON path.

TypeBox offers a few of ways to access interior properties via string. I had originally opted for Json Pointer (RFC 6901) as Json Schema uses this spec for $ref referencing, but I am certainly open other string referencing formats if they have formal RFC's (IETF especially) + some observed usage in applications.

JsonPath does fit the RFC criteria, https://datatracker.ietf.org/doc/rfc9535/ and I quite like the idea of having a XPath equivalent for Json, so I do think this could be something worth looking into.

Just for reference though, the current string accessor methods are shown below.


Json Pointer

The implementation for Json Pointer (RFC 6901) lives under the Value submodule.

Reference Link

import { ValuePointer } from '@sinclair/typebox/value'
import { Type } from '@sinclair/typebox'

const schema = Type.Object({
  array: Type.Array(
    Type.Object({
      string: Type.String(),
    })
  ),
})

const result = ValuePointer.Get(schema, '/properties/array/items/properties/string')

console.log(result) // { type: 'string', [Symbol(TypeBox.Kind)]: 'String' }

Index Access Types

Index Access types are another option. This approach intends to mirror the semantics of TypeScript and does provide inference support, but is limited to TypeBox types only.

Reference Link

import { Type } from '@sinclair/typebox'
import { Syntax } from '@sinclair/typebox/syntax'

const schema = Type.Object({
  array: Type.Array(
    Type.Object({
      string: Type.String(),
    })
  )
})

const result = Syntax({ schema }, `schema['array'][number]['string']`)

// result is TString

I'll do some reading up. I'm wondering if JsonPath could match the interface for ValuePointer or if the interface for it should be limited to only readonly (i.e. Query(value, path)). Also, I am wondering about Json document databases, systems like Mongo and whether or not they provide support for Json Path. Would be interesting to do a bit of research just to find out what systems are using the format.

Let's chat more on this, I think it could be good to consider adding. Would you be interested in helping to contribute an implementation?

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

No branches or pull requests

2 participants