evanw/esbuild · error · Error

key ${quote(key)} in object ${quote(property)} must be a str

Error message

key ${quote(key)} in object ${quote(property)} must be a string

What it means

Import attributes (the 'with' option in resolve(), or the with field from onResolve/onLoad) must be a map of string→string because esbuild forwards them verbatim to the native resolver over the stdio protocol, which only accepts string values. sanitizeStringMap (lib/shared/common.ts:1818) throws at :1822 when any value is not a string — e.g. with: { type: 'json' } is fine, but with: { count: 3 } throws. The message includes the offending key and the parent property name.

Source

Thrown at lib/shared/common.ts:1822

  }

  return messagesClone
}

function sanitizeStringArray(values: any[], property: string): string[] {
  const result: string[] = []
  for (const value of values) {
    if (typeof value !== 'string') throw new Error(`${quote(property)} must be an array of strings`)
    result.push(value)
  }
  return result
}

function sanitizeStringMap(map: Record<string, any>, property: string): Record<string, string> {
  const result: Record<string, string> = Object.create(null)
  for (const key in map) {
    const value = map[key]
    if (typeof value !== 'string') throw new Error(`key ${quote(key)} in object ${quote(property)} must be a string`)
    result[key] = value
  }
  return result
}

function convertOutputFiles({ path, contents, hash }: protocol.BuildOutputFile): types.OutputFile {
  // The text is lazily-generated for performance reasons. If no one asks for
  // it, then it never needs to be generated.
  let text: string | null = null
  return {
    path,
    contents,
    hash,
    get text() {
      // People want to be able to set "contents" and have esbuild automatically
      // derive "text" for them, so grab the contents off of this object instead
      // of using our original value.
      const binary = this.contents

View on GitHub (pinned to 6ff1d8b0d8)

Solutions

  1. Stringify all values: with: Object.fromEntries(Object.entries(attrs).map(([k,v]) => [k, String(v)])).
  2. Only put import attributes (like type: 'json') into 'with'; route other metadata via pluginData.
  3. Validate at plugin boundary: for (const k in with_) if (typeof with_[k] !== 'string') throw.

Example fix

// before
build.resolve('./data.json', {
  kind: 'import-statement',
  with: { type: 'json', priority: 1 },
});
// after
build.resolve('./data.json', {
  kind: 'import-statement',
  with: { type: 'json', priority: '1' },
});
Defensive patterns

Strategy: validation

Validate before calling

function asStringMap(v, name) {
  const out = Object.create(null);
  for (const k in v) {
    if (typeof v[k] !== 'string') {
      throw new TypeError(`key ${JSON.stringify(k)} in object ${JSON.stringify(name)} must be a string`);
    }
    out[k] = v[k];
  }
  return out;
}

Type guard

function isStringMap(v): v is Record<string, string> {
  return !!v && typeof v === 'object' && !Array.isArray(v)
    && Object.values(v).every(x => typeof x === 'string');
}

Prevention

When it happens

Trigger: resolve(path, { kind, with: { type: 'json', retries: 5 } }) — retries is a number. onLoad returning { contents, with: { ...nonString } }. Plugin forwards arbitrary user metadata into with.

Common situations: Treating 'with' as a generic metadata bag instead of an import-attributes map. Migration from the deprecated assert syntax where values happened to be strings. Plugin authors passing through JSON from a config that has numbers/booleans.

Related errors


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/616cd557bc8c5eb8.json. Report an issue: GitHub.