nexu-io/open-design · error

${field} must be a string

Error message

${field} must be a string

What it means

optionalString (refresh.ts:512-516) is a typed accessor used by selectJsonPath (for input.path/file/name) and selectGithubRepositoryApiUrl (for input.url). It returns undefined when the field is absent but throws when the field is present and not a string, so callers cannot mistake a wrong-typed field for a missing one.

Source

Thrown at apps/daemon/src/live-artifacts/refresh.ts:514

    const source = options.artifact.document.sourceJson;
    const mapped = source.toolName === 'public_github_repository_metric' && source.outputMapping?.dataPaths !== undefined
      ? asBoundedRefreshOutput(applyDataPaths(options.documentOutput.output, source.outputMapping.dataPaths))
      : applyLiveArtifactOutputMapping({
          source,
          output: options.documentOutput.output,
        });
    deepMergeBoundedJsonObject(dataJson, mapped);
    if (source.toolName === 'public_github_repository_metric') {
      applyLegacyGithubRepositoryMetricCompat(dataJson, options.documentOutput.output);
    }
  }

  return { dataJson: asBoundedRefreshOutput(dataJson) };
}

function optionalString(value: BoundedJsonValue | undefined, field: string): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'string') throw new Error(`${field} must be a string`);
  return value;
}

function optionalPositiveInteger(value: BoundedJsonValue | undefined, field: string, defaultValue: number, maxValue: number): number {
  if (value === undefined) return defaultValue;
  if (!Number.isSafeInteger(value) || typeof value !== 'number' || value < 1) throw new Error(`${field} must be a positive integer`);
  return Math.min(value, maxValue);
}

function selectJsonPath(input: ProjectFilesReadJsonInput): string {
  const rawPath = optionalString(input.path, 'input.path') ?? optionalString(input.file, 'input.file') ?? optionalString(input.name, 'input.name');
  if (rawPath === undefined) throw new Error('project_files.read_json requires input.path');
  return validateProjectPath(rawPath);
}

function compactTextPreview(text: string, query: string | undefined): string {
  const normalized = text.replace(/\s+/g, ' ').trim();
  if (normalized.length <= 240) return normalized;

View on GitHub (pinned to 5be4028344)

Solutions

  1. Pass the field as a string (e.g. input.path: 'data.json', input.url: 'https://...').
  2. Coerce known-safe values to string before constructing the input.
  3. Validate the input shape against the tool's input schema before refresh.

Example fix

// before
const input = { path: 42 };
// throws `input.path must be a string`

// after
const input = { path: 'data.json' };
Defensive patterns

Strategy: type-guard

Validate before calling

function optionalString(v: unknown, field: string): string | undefined {
  if (v === undefined) return undefined;
  if (typeof v !== 'string') throw new Error(`${field} must be a string`);
  return v;
}

Type guard

function isOptionalString(v: unknown): v is string | undefined {
  return v === undefined || typeof v === 'string';
}

Prevention

When it happens

Trigger: A refresh source.input provides path/file/name (for project_files.read_json) or url (for public_github_repository_metric) as a number, boolean, object, or array instead of a string.

Common situations: A connector/tool input built programmatically that passes a numeric path or a parsed-JSON object where a string was expected; a migration that changed a field's type; user input not coerced to string.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/defe0bc1e41a22a7. Report an issue: GitHub.