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
- Open the entry's text and validate it with JSON.parse locally; fix syntax errors (trailing commas, truncation)
- Ensure the top level is an object: wrap bare arrays or scalars in an object in the source file
- Re-export/re-save the file with UTF-8 encoding without BOM, confirming it is valid JSON
- 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
- Validate every .json file in the model ZIP with JSON.parse before shipping it
- Save JSON as UTF-8 without BOM; avoid binary content under a .json extension
- Ensure top-level structure is an object, not an array or scalar
- Avoid hand edits that leave trailing commas or truncated files; use a linter/formatter for JSON
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
- Empty settings file: ${url}
- Expected exactly one moc file, got ${mocFiles.length} ${file
- Textures not found
- Cannot find file: ${path}
- [ZipLoader] Failed to extract CDI/EXP metadata:
AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02).
Data as JSON: /api/errors/bc1b466e5b4e1980.
Report an issue: GitHub.