doba

Identify

API reference for schema identification

API reference for the Schema Identification feature. All exports are available from dobajs.

Configuration

IdentifyConfig

The identify option on RegistryConfig accepts one of two forms:

type IdentifyConfig<Keys extends string> =
  | IdentifyGuardMap<Keys>   // guard map: per-schema predicates
  | IdentifyFn               // function: single discriminator

When provided, the registry gains identify() and identifyAndTransform() methods. When omitted, those methods don't exist on the type.

IdentifyGuardMap

A partial record mapping schema keys to guards. Each value is either an IdentifyGuard or the tryParse sentinel.

type IdentifyGuardMap<Keys extends string> = Partial<
  Readonly<Record<Keys, IdentifyGuard | TryParse>>
>

Guards run in definition order. First true wins. Keys are typed against the schema map, so typos are compile-time errors. You don't have to cover every schema.

IdentifyGuard

type IdentifyGuard = (value: unknown) => boolean

A sync predicate that tests whether an unknown value belongs to a particular schema. Used as values in an IdentifyGuardMap, or built via the match helper.

IdentifyFn

A single discriminator function that returns a schema key or null.

type IdentifyFn = (value: unknown) => string | null

Returns string | null (not narrowed to Keys) so helpers like byField work without knowing the schema map at definition time. Returned keys are verified against registered schemas at runtime.

Result types

IdentifyResult

Returned by registry.identify().

type IdentifyResult<Keys extends string = string> = Result<
  Keys,
  readonly DobaIssue[],
  IdentifyMeta<Keys>
>

IdentifyMeta

type IdentifyMeta<Keys extends string = string> = {
  readonly schema: Keys
}

On success:

FieldTypeDescription
valueKeysThe matched schema key.
meta.schemaKeysSame as value.

On failure:

FieldTypeDescription
issues[].code'identify_failed' | 'identify_ambiguous'What went wrong.
issues[].messagestringHuman-readable description.
issues[].metaRecord<string, unknown>For identify_ambiguous: { matches: string[] }.

IdentifyTransformResult

Returned by registry.identifyAndTransform(). Same shape as TransformResult but meta includes from.

type IdentifyTransformResult<T, Keys extends string = string> = Result<
  T,
  readonly DobaIssue[],
  IdentifyTransformMeta<Keys>
>

IdentifyTransformMeta

type IdentifyTransformMeta<Keys extends string = string> = TransformMeta<Keys> & {
  readonly from: Keys
}

On success:

FieldTypeDescription
valueTThe transformed value, typed to the target schema output.
meta.fromKeysThe detected source schema.
meta.pathreadonly Keys[]Ordered schema keys traversed, e.g. ['v1', 'v2', 'v3'].
meta.stepsreadonly StepInfo[]Per-step metadata for each migration executed.
meta.warningsreadonly WarningInfo[]Warnings from ctx.warn() and deprecated migrations.
meta.defaultsreadonly DefaultedInfo[]Fields filled with default values during migration.

Registry methods

identify()

registry.identify(value: unknown): Promise<IdentifyResult<Keys>>

Detects which schema an unknown value belongs to. Runs sync guards first (in definition order, first true wins), then tries tryParse schemas in parallel if no sync guard matched.

OutcomeIssue code
No guard matched, no tryParse validatedidentify_failed
Multiple tryParse schemas validatedidentify_ambiguous
const result = await registry.identify(unknownData)

if (result.ok) {
  console.log(result.value)       // "database"
  console.log(result.meta.schema) // "database"
} else {
  console.log(result.issues[0].code) // "identify_failed" or "identify_ambiguous"
}

identifyAndTransform()

registry.identifyAndTransform<To extends Keys>(
  value: unknown,
  to: To,
  options?: TransformOptions<Keys>,
): Promise<IdentifyTransformResult<InferOutput<Schemas[To]>, Keys>>

Identifies the source schema, then transforms to the target. Combines identify() and transform() in one call. Accepts all TransformOptions (validate, path, pathStrategy, validatePath).

const result = await registry.identifyAndTransform(unknownData, 'frontend')

if (result.ok) {
  result.value      // typed as FrontendUser
  result.meta.from  // "database" (detected source)
  result.meta.path  // ["database", "frontend"]
  result.meta.steps // per-step metadata
}

Helpers

match

Entry point for building chainable identify guards. Returns a Matcher.

const match: Matcher
import { match } from 'dobajs'

match.field('passwordHash')                    // field exists
match.field('version', 2)                      // field === 2
match.fields('id', 'email')                    // both fields exist
match.type('string')                           // typeof check
match.test((v) => Array.isArray(v))            // custom predicate
match.field('passwordHash').field('email')     // AND: both must exist

Matcher

Each method returns a new Matcher that is both chainable (add more conditions) and callable as (value: unknown) => boolean. Chaining ANDs conditions together.

interface Matcher {
  (value: unknown): boolean

  field(name: string, expected?: unknown): Matcher
  fields(...names: string[]): Matcher
  type(type: string): Matcher
  test(fn: (value: unknown) => boolean): Matcher
}
MethodWhat it checks
.field(name)Field exists on the value (name in value).
.field(name, val)Field exists and value[name] === val (strict equality).
.fields(...names)All named fields exist on the value.
.type(t)typeof value === t. Note: typeof null === 'object' in JS.
.test(fn)Custom predicate returns true.

tryParse

const tryParse: unique symbol

A sentinel symbol. When used as a guard value in the identify map, doba validates the value against that schema's ~standard.validate() instead of running a sync predicate.

Sync guards always run before tryParse. Multiple tryParse schemas are validated in parallel. If more than one validates, the result is identify_ambiguous.

import { tryParse } from 'dobajs'

identify: {
  database: match.field('passwordHash'),  // sync guard (runs first)
  frontend: tryParse,                      // schema validation fallback
}

byField()

function byField(
  field: string,
  options?: ByFieldOptions,
): (value: unknown) => string | null

type ByFieldOptions =
  | { prefix?: string; suffix?: string; map?: never }
  | { map: Record<string, string>; prefix?: never; suffix?: never }

Creates a discriminator function that reads a field from the value and derives a schema key. Returns null if the value isn't an object, the field is missing, or (with map) no mapping exists. Field values are converted via String(), so numeric values like { version: 2 } become "2".

OptionEffectExample
(none)String(value[field]) used as key directly{ version: "v1" } -> "v1"
prefixPrepended to the stringified field value{ v: "1" } + prefix "v" -> "v1"
suffixAppended to the stringified field value{ kind: "user" } + suffix "_v2" -> "user_v2"
mapExplicit lookup (mutually exclusive with prefix/suffix){ type: "UserDB" } + map { UserDB: "database" } -> "database"

firstMatch()

function firstMatch(
  ...fns: readonly ((value: unknown) => string | null)[],
): (value: unknown) => string | null

Composes multiple discriminator functions. Tries each in order, returns the first non-null result. Returns null if all functions return null.

import { byField, firstMatch } from 'dobajs'

identify: firstMatch(
  byField('_tag'),
  byField('version', { prefix: 'v' }),
  (v) => typeof v === 'string' ? 'name' : null,
)

Issue codes

CodeWhenExtra metadata
identify_failedNo guard matched and no tryParse schema validated
identify_ambiguousMultiple tryParse schemas validated the same valuemeta.matches: string[] listing conflicting schemas

On this page