mifi/lossless-cut · warning · UserFacingError
"{{property}}" must be an object of strings
Error message
"{{property}}" must be an object of strings What it means
Thrown by mutateSegment (useSegments.tsx:832) when `response.tags` fails to parse against `segmentTagsSchema`, defined as `z.record(z.string(), z.string())` in types.ts:23 — i.e. tags must be a plain object whose keys and values are all strings. Arrays, primitives, null, or objects containing non-string values (numbers, booleans, nested objects) all fail safeParse and raise this UserFacingError. Surfaced as 'Expression failed: "tags" must be an object of strings'.
Source
Thrown at src/renderer/src/hooks/useSegments.tsx:832
async function mutateSegment(seg: StateSegment, index: number, expr: string) {
const response = (await safeishEval(expr, { segment: getScopeSegment(seg, index) }));
invariant(typeof response === 'object' && response != null, i18n.t('The expression must return an object'));
const ret: Partial<Pick<StateSegment, 'name' | 'start' | 'end' | 'tags'>> = {};
if ('label' in response) {
if (typeof response.label !== 'string') throw new UserFacingError(i18n.t('"{{property}}" must be a string', { property: 'label' }));
ret.name = response.label;
}
if ('start' in response) {
if (typeof response.start !== 'number') throw new UserFacingError(i18n.t('"{{property}}" must be a number', { property: 'start' }));
ret.start = response.start;
}
if ('end' in response) {
if (!(typeof response.end === 'number' || response.end === undefined)) throw new UserFacingError(i18n.t('"{{property}}" must be a number', { property: 'end' }));
ret.end = response.end;
}
if ('tags' in response) {
const tags = segmentTagsSchema.safeParse(response.tags);
if (!tags.success) throw new UserFacingError(i18n.t('"{{property}}" must be an object of strings', { property: 'tags' }));
ret.tags = tags.data;
}
return ret;
}
const mutateSegments = async (expr: string) => (await pMap(cutSegments, async (seg, index) => ({
...seg,
...(seg.selected && await mutateSegment(seg, index, expr)),
}), { concurrency: 5 })).flat();
const onSubmit = async (value: string) => {
try {
if (value.trim().length === 0) return { error: i18n.t('Please enter a JavaScript expression.') };
const mutated = await mutateSegments(value);
safeSetCutSegments(mutated, fileDuration);
return undefined;View on GitHub (pinned to 3b9a59c288)
Solutions
- Coerce every non-string value with String(): `{ tags: { count: String(segment.index) } }`
- Use string literal values: `{ tags: { even: 'true' } }` (string 'true', not boolean)
- Spread existing tags and only add string values: `{ tags: { ...segment.tags, newKey: 'val' } }`
- If passing through segment.tags unchanged, omit the `tags` key from the returned object instead of re-assigning it
Example fix
// before (numeric tag value fails z.record(string,string))
{ tags: { count: segment.index } }
// after
{ tags: { count: String(segment.index) } } Defensive patterns
Strategy: validation
Validate before calling
import { segmentTagsSchema } from '../types';
function validateMutateResponse(response: unknown): string | null {
if (typeof response !== 'object' || response === null)
return 'The expression must return an object';
const r = response as Record<string, unknown>;
if ('tags' in r && !segmentTagsSchema.safeParse(r.tags).success)
return '"tags" must be an object of strings (Record<string, string>)';
return null;
} Type guard
import { segmentTagsSchema, type SegmentTags } from '../types';
function hasValidTags(r: object): r is { tags: SegmentTags } & object {
if (!('tags' in r)) return false;
return segmentTagsSchema.safeParse((r as { tags?: unknown }).tags).success;
} Try / catch
// safeParse already runs before the throw (useSegments.tsx:831).
// To give users actionable detail, surface zod's issue in the catch:
import { ZodError } from 'zod';
// inside mutateSegment, replace the throw with:
if (!tags.success)
throw new UserFacingError(
i18n.t('"{{property}}" must be an object of strings ({{detail}})',
{ property: 'tags', detail: (tags.error as ZodError).issues[0]?.message ?? '' }),
); Prevention
- Coerce every tag value with String() — never store numbers or booleans directly
- Remember tags is a Record (key→string object), not an array
- When spreading segment.tags, only add string-valued keys
- If you only want to preserve existing tags, omit the `tags` key from the result
When it happens
Trigger: Returning `{ tags: ['a','b'] }` (array, not record), `{ tags: 'tag' }` (string), `{ tags: { count: segment.index } }` (numeric value), `{ tags: { flag: true } }` (boolean value), `{ tags: { nested: { a: 'b' } } }` (nested object), or `{ tags: null }`.
Common situations: Users store a numeric counter or boolean flag as a tag value without coercing to string. Migrating from an array-based tag mental model. Spreading segment.tags then adding a numeric property: `{ tags: { ...segment.tags, count: segment.index } }` — the spread is fine but the added number trips the schema.
Related errors
- "{{property}}" must be a string
- "{{property}}" must be a number
- No rows found
- Invalid start or end value. Must contain a number of seconds
- Segment start time must precede end time
AI-assisted analysis of mifi/lossless-cut@3b9a59c288 (2026-08-12).
Data as JSON: /api/errors/533da41106351b82.
Report an issue: GitHub.