moeru-ai/airi · error · TypeError

Expected a JSON object in ${filePath}

Error message

Expected a JSON object in ${filePath}

What it means

After successfully reading an archive entry, readJsonObject parses its text with JSON.parse and requires the result to be a plain object (isRecord). It throws a TypeError when the entry's content is not a JSON object — malformed JSON, or valid JSON whose top level is an array, string, number, or null. Live2D metadata files are expected to be objects.

Source

Thrown at packages/stage-ui-live2d/src/utils/live2d-validator.ts:187

      if (!isRecord(definition))
        continue

      const file = readString(definition.File)
      if (file)
        references.push(file)
    }
  }
  return references
}

async function readJsonObject(zip: JSZip, filePath: string): Promise<Record<string, unknown>> {
  const file = zip.file(filePath)
  if (!file)
    throw new Error(`Archive entry not found: ${filePath}`)

  const value: unknown = JSON.parse(await file.async('text'))
  if (!isRecord(value))
    throw new TypeError(`Expected a JSON object in ${filePath}`)
  return value
}

function addIssue(
  report: Live2DValidationReport,
  code: Live2DValidationIssueCode,
  severity: Live2DValidationIssueSeverity,
  message: string,
  resolution: string,
): void {
  report.issues.push({ code, severity, message, resolution })
}

function checkReference(
  report: Live2DValidationReport,
  archivePaths: string[],
  options: ReferenceCheckOptions,
): boolean {

View on GitHub (pinned to 9c213115f8)

Solutions

  1. Open the entry's text and validate it with JSON.parse locally; fix syntax errors (trailing commas, truncation)
  2. Ensure the top level is an object: wrap bare arrays or scalars in an object in the source file
  3. Re-export/re-save the file with UTF-8 encoding without BOM, confirming it is valid JSON
  4. Replace files whose extension is .json but whose content is HTML or binary

Example fix

// before (file content)
[{ "id": "motion" }]
// after
{ "motions": [{ "id": "motion" }] }
Defensive patterns

Strategy: type-guard

Validate before calling

function isJsonObjectText(text: string): boolean {
  try {
    const v: unknown = JSON.parse(text)
    return typeof v === 'object' && v !== null && !Array.isArray(v)
  } catch { return false }
}
// verify each .json entry's text with isJsonObjectText before validating

Type guard

function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v)
}

Try / catch

try {
  const obj = await readJsonObject(zip, filePath)
} catch (e) {
  if (e instanceof TypeError && e.message.startsWith('Expected a JSON object in')) {
    // report the file path; inspect its raw text to find the malformed content
  } else throw e
}

Prevention

When it happens

Trigger: Calling validateLive2DZip (or displayInfo/countParsedJsonResources) on a ZIP where a JSON entry is truncated/corrupted during compression, is a JSON array instead of an object, or is not JSON at all despite its extension (e.g. an HTML error page saved as .json).

Common situations: Build tools minifying/transforming JSON into non-object output; text-encoding corruption (BOM or binary content) breaking JSON.parse; exporting settings that serialize as arrays; hand-edited JSON with trailing commas.

Related errors


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02). Data as JSON: /api/errors/bc1b466e5b4e1980. Report an issue: GitHub.