BabylonJS/Babylon.js · error

ProjectFile: Invalid project bundle — missing project.json

Error message

ProjectFile: Invalid project bundle — missing project.json

What it means

LoadProjectFileAsync unzips a project bundle and expects an entry literally named 'project.json' at the archive root. If the zip contains no such entry, the bundle is considered invalid and the promise rejects with this error.

Source

Thrown at packages/dev/sharedUiComponents/src/projects/projectFile.ts:403

/**
 * Loads a `.babylonproj` zip bundle into a scene. Extracts all files, creates
 * blob URLs for bundled assets, and loads the project through SAM.
 *
 * @param scene - The scene to load the project into.
 * @param zipFile - The `.babylonproj` zip file to load.
 */
export async function LoadProjectFileAsync(scene: Scene, zipFile: File): Promise<void> {
    const arrayBuffer = await zipFile.arrayBuffer();
    const { unzip, strFromU8 } = await import("fflate");
    const extracted = await new Promise<Record<string, Uint8Array>>((resolve, reject) => {
        unzip(new Uint8Array(arrayBuffer), (err, data) => (err ? reject(err instanceof Error ? err : new Error(String(err))) : resolve(data)));
    });

    // Parse project.json
    const projectJsonBytes = extracted["project.json"];
    if (!projectJsonBytes) {
        throw new Error("ProjectFile: Invalid project bundle — missing project.json");
    }
    const projectJson = JSON.parse(strFromU8(projectJsonBytes));

    // Create blob URLs for all bundled files and rewrite asset URLs
    for (const [, entry] of Object.entries(projectJson.assets as Record<string, { url: string }>)) {
        const filename = entry.url;
        const fileBytes = extracted[filename];
        if (fileBytes) {
            const mimeType = GuessMimeType(filename);
            // Use a named File so LoadAssetContainerAsync can detect the
            // format from the filename (blob URLs alone have no extension).
            const file = new File([fileBytes as BlobPart], filename, { type: mimeType });
            const blobUrl = URL.createObjectURL(file);
            entry.url = blobUrl;
        }
        // If no file found in zip, assume the URL is a remote reference — leave it as-is
    }

View on GitHub (pinned to 0592b347b8)

Solutions

  1. Re-create the zip so project.json sits at the archive root (select the folder's CONTENTS, not the folder itself).
  2. Verify the bundle contents by listing the zip entries before loading.
  3. Ensure you downloaded the project bundle (not a partial assets archive).
  4. Re-export the project from the editor to produce a valid bundle.

Example fix

// before (zip layout)
myProject/project.json
myProject/assets/...
// after (zip layout)
project.json
assets/...
Defensive patterns

Strategy: validation

Validate before calling

const zip = await JSZip.loadAsync(arrayBuffer);
if (!zip.file('project.json')) {
    throw new Error('bundle is missing root project.json');
}
await LoadProjectFileAsync(arrayBuffer, ...);

Type guard

function bundleHasManifest(entries: Record<string, Uint8Array>): entries is Record<string, Uint8Array> & { 'project.json': Uint8Array } {
    return Boolean(entries['project.json']);
}

Try / catch

try {
    await LoadProjectFileAsync(buffer, name);
} catch (e) {
    if (e instanceof Error && e.message.includes('missing project.json')) {
        // prompt user to re-upload a valid project bundle
    } else throw e;
}

Prevention

When it happens

Trigger: Passing an ArrayBuffer of a zip that lacks a root-level project.json — e.g. the project.json is nested in a subfolder, the zip contains only assets, or the wrong file was uploaded.

Common situations: Re-zipping an extracted project with an extra wrapping folder (macOS Archive Utility / right-click compress), uploading the assets zip instead of the project bundle, or exporting from a tool that names the manifest differently.

Related errors


AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30). Data as JSON: /api/errors/31da952cbd6c504b. Report an issue: GitHub.