moeru-ai/airi · error · InvalidCharacterCardError

Invalid Character Card V3.

Error message

Invalid Character Card V3.

What it means

parseCharacterCardV3 runs the Valibot characterCardV3Schema (via safeParse) over the decoded source and throws InvalidCharacterCardError when validation fails; the Valibot issues are attached as `cause` and the original input as `source`. Unknown fields are preserved and older/newer spec_version values are tolerated, so a throw indicates genuine structural problems such as missing or wrongly typed required fields.

Solutions

  1. Catch the error and inspect `cause` — it holds the Valibot issues with exact paths (e.g. data.name missing)
  2. Check the V3 contract: spec === 'chara_card_v3', spec_version string, and required data fields present with correct types
  3. If the card is V2 (spec 'chara_card_v2'), convert/upgrade it to V3 before parsing
  4. Fix the source and re-parse; unknown extra fields do not need removal

Example fix

// before
const { card } = parseCharacterCardV3(raw) // Error: Invalid Character Card V3.

// after
import { parseCharacterCardV3, isInvalidCharacterCardError } from '@proj-airi/ccc'
try {
  const { card } = parseCharacterCardV3(raw)
}
catch (err) {
  if (isInvalidCharacterCardError(err)) {
    for (const issue of err.cause ?? [])
      console.error(issue.path?.join('.'), issue.message)
  }
  throw err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Optional pre-check with the same schema machinery before committing to a parse
import { safeParse } from 'valibot'
const check = safeParse(characterCardV3Schema, typeof raw === 'string' ? JSON.parse(raw) : raw)
if (!check.success) {
  showIssues(check.issues) // field-level feedback before the throwing API
}

Type guard

import { isInvalidCharacterCardError } from '@proj-airi/ccc'
// isInvalidCharacterCardError(error): error is InvalidCharacterCardError
// narrowing via instanceof, exposes .cause (Valibot issues) and .source

Try / catch

try {
  const { card, compatibility } = parseCharacterCardV3(source)
}
catch (err) {
  if (isInvalidCharacterCardError(err)) {
    // err.cause holds Valibot issues (schema failure) or a SyntaxError (JSON failure)
    reportCardIssues(err.cause)
    return
  }
  throw err
}

Prevention

When it happens

Trigger: A card missing required fields (spec, spec_version, data.name), wrong field types (data.description not a string, character_book entries malformed), or a V2 card passed straight into the V3 parser without conversion.

Common situations: Importing cards exported by other frontends with subtle schema drift; hand-edited JSON that dropped a required key; upstream generators emitting null for optional-but-typed fields; truncated downloads.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/4fe09063add39858. Report an issue: GitHub.

Appendix: source

Thrown at packages/ccc/src/codec/characterCardV3.ts:179

/** Checks whether an error came from the CCv3 parsing boundary. */
export function isInvalidCharacterCardError(error: unknown): error is InvalidCharacterCardError {
  return error instanceof InvalidCharacterCardError
}

/**
 * Parses and validates a Character Card V3 object or JSON document.
 *
 * Unknown fields are preserved so a newer card can be inspected and exported
 * without silently discarding data AIRI does not understand yet. Older and
 * newer `spec_version` values are accepted and reported through
 * `compatibility`; callers can decide how prominently to warn users.
 */
export function parseCharacterCardV3(source: unknown): ParsedCharacterCardV3 {
  const candidate = parseJsonSource(source)
  const result = safeParse(characterCardV3Schema, candidate)

  if (!result.success) {
    throw new InvalidCharacterCardError({
      cause: result.issues,
      source,
    })
  }

  return {
    card: result.output,
    compatibility: resolveCompatibility(result.output.spec_version),
  }
}

function parseJsonSource(source: unknown): unknown {
  if (typeof source !== 'string')
    return source

  try {
    return JSON.parse(source)
  }

View on GitHub (pinned to 677329427f)