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.contentsView on GitHub (pinned to 6ff1d8b0d8)
Solutions
- Stringify all values: with: Object.fromEntries(Object.entries(attrs).map(([k,v]) => [k, String(v)])).
- Only put import attributes (like type: 'json') into 'with'; route other metadata via pluginData.
- 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
- Only put import attributes (like type: 'json') into the 'with' map; route other metadata via pluginData.
- Stringify non-string values before placing them in 'with'.
- Validate plugin-supplied import attributes at the plugin boundary.
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
- Plugin at index ${i} must be an object
- Plugin at index ${i} is missing a name
- Plugin is missing a setup function
- Must specify "kind" when calling "resolve"
- onResolve() call is missing a filter
AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03).
Data as JSON: /data/errors/616cd557bc8c5eb8.json.
Report an issue: GitHub.