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

  1. Coerce every non-string value with String(): `{ tags: { count: String(segment.index) } }`
  2. Use string literal values: `{ tags: { even: 'true' } }` (string 'true', not boolean)
  3. Spread existing tags and only add string values: `{ tags: { ...segment.tags, newKey: 'val' } }`
  4. 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

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


AI-assisted analysis of mifi/lossless-cut@3b9a59c288 (2026-08-12). Data as JSON: /api/errors/533da41106351b82. Report an issue: GitHub.