Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/quiet-masks-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@mastra/client-js': patch
'@mastra/core': patch
'@mastra/hono': patch
'@mastra/server': patch
'@mastra/pg': patch
'@mastra/libsql': patch
'@mastra/playground-ui': patch
---

Dataset schemas now appear in the Edit Dataset dialog. Previously the `inputSchema` and `groundTruthSchema` fields were not passed to the dialog, so editing a dataset always showed empty schemas.

Schema edits in the JSON editor no longer cause the cursor to jump to the top of the field. Typing `{"type": "object"}` in the schema editor now behaves like a normal text input instead of resetting on every keystroke.

Validation errors are now surfaced when updating a dataset schema that conflicts with existing items. For example, adding a `required: ["name"]` constraint when existing items lack a `name` field now shows "2 existing item(s) fail validation" in the dialog instead of silently dropping the error.

Disabling a dataset schema from the Studio UI now correctly clears it. Previously the server converted `null` (disable) to `undefined` (no change), so the old schema persisted and validation continued.

Workflow schemas fetched via `client.getWorkflow().getSchema()` are now correctly parsed. The server serializes schemas with `superjson`, but the client was using plain `JSON.parse`, yielding a `{json: {...}}` wrapper instead of the actual JSON Schema object.
13 changes: 10 additions & 3 deletions client-sdks/client-js/src/resources/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type {
GetWorkflowRunByIdResponse,
} from '../types';

import { parseClientRequestContext, base64RequestContext, requestContextQueryString } from '../utils';
import {
parseClientRequestContext,
base64RequestContext,
requestContextQueryString,
parseSuperJsonString,
} from '../utils';
import { BaseResource } from './base';
import { Run } from './run';

Expand Down Expand Up @@ -140,8 +145,10 @@ export class Workflow extends BaseResource {
}> {
const details = await this.details();
return {
inputSchema: details.inputSchema ? JSON.parse(details.inputSchema) : null,
outputSchema: details.outputSchema ? JSON.parse(details.outputSchema) : null,
inputSchema: details.inputSchema ? (parseSuperJsonString(details.inputSchema) as Record<string, unknown>) : null,
outputSchema: details.outputSchema
? (parseSuperJsonString(details.outputSchema) as Record<string, unknown>)
: null,
};
}

Expand Down
4 changes: 2 additions & 2 deletions client-sdks/client-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1883,8 +1883,8 @@ export interface DatasetRecord {
name: string;
description?: string | null;
metadata?: Record<string, unknown> | null;
inputSchema?: Record<string, unknown> | null;
groundTruthSchema?: Record<string, unknown> | null;
inputSchema?: Record<string, unknown>;
groundTruthSchema?: Record<string, unknown>;
version: number;
createdAt: string | Date;
updatedAt: string | Date;
Expand Down
14 changes: 14 additions & 0 deletions client-sdks/client-js/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ export function toQueryParams<T extends Record<string, unknown>>(params: T, flat
return searchParams.toString();
}

/**
* Parses a JSON string that may have been serialized with superjson.
* superjson wraps values as `{json: ..., meta: ...}` — this unwraps to the inner value.
* Also handles plain JSON strings for forward compatibility.
* @throws {SyntaxError} if `value` is not valid JSON
*/
export function parseSuperJsonString(value: string): unknown {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'json' in parsed) {
return parsed.json;
}
return parsed;
}

export function parseClientRequestContext(requestContext?: RequestContext | Record<string, any>) {
if (requestContext) {
if (requestContext instanceof RequestContext) {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/datasets/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,18 @@ export class Dataset {

let { inputSchema, groundTruthSchema, ...rest } = input;

if (inputSchema !== undefined && isZodType(inputSchema)) {
if (inputSchema !== undefined && inputSchema !== null && isZodType(inputSchema)) {
inputSchema = zodToJsonSchema(inputSchema);
}
if (groundTruthSchema !== undefined && isZodType(groundTruthSchema)) {
if (groundTruthSchema !== undefined && groundTruthSchema !== null && isZodType(groundTruthSchema)) {
groundTruthSchema = zodToJsonSchema(groundTruthSchema);
}

return store.updateDataset({
id: this.id,
...rest,
inputSchema: inputSchema as Record<string, unknown> | undefined,
groundTruthSchema: groundTruthSchema as Record<string, unknown> | undefined,
inputSchema: inputSchema as Record<string, unknown> | null | undefined,
groundTruthSchema: groundTruthSchema as Record<string, unknown> | null | undefined,
});
}

Expand Down
32 changes: 24 additions & 8 deletions packages/core/src/storage/domains/datasets/inmemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ function toDatasetItem(row: DatasetItemRow): DatasetItem {
};
}

/** Internal record that allows null schemas (for "clear schema" semantics) */
type InternalDatasetRecord = Omit<DatasetRecord, 'inputSchema' | 'groundTruthSchema'> & {
inputSchema?: Record<string, unknown> | null;
groundTruthSchema?: Record<string, unknown> | null;
};

/** Normalize internal record (which may have null schemas) to public DatasetRecord */
function toDatasetRecord(record: InternalDatasetRecord): DatasetRecord {
return {
...record,
inputSchema: record.inputSchema ?? undefined,
groundTruthSchema: record.groundTruthSchema ?? undefined,
};
}

export class DatasetsInMemory extends DatasetsStorage {
private db: InMemoryDB;

Expand All @@ -52,7 +67,7 @@ export class DatasetsInMemory extends DatasetsStorage {
async createDataset(input: CreateDatasetInput): Promise<DatasetRecord> {
const id = crypto.randomUUID();
const now = new Date();
const dataset: DatasetRecord = {
const dataset = {
id,
name: input.name,
description: input.description,
Expand All @@ -62,13 +77,14 @@ export class DatasetsInMemory extends DatasetsStorage {
version: 0,
createdAt: now,
updatedAt: now,
};
} as DatasetRecord;
this.db.datasets.set(id, dataset);
return dataset;
return toDatasetRecord(dataset);
}

async getDatasetById({ id }: { id: string }): Promise<DatasetRecord | null> {
return this.db.datasets.get(id) ?? null;
const record = this.db.datasets.get(id);
return record ? toDatasetRecord(record) : null;
}

protected async _doUpdateDataset(args: UpdateDatasetInput): Promise<DatasetRecord> {
Expand All @@ -77,17 +93,17 @@ export class DatasetsInMemory extends DatasetsStorage {
throw new Error(`Dataset not found: ${args.id}`);
}

const updated: DatasetRecord = {
const updated = {
...existing,
name: args.name ?? existing.name,
description: args.description ?? existing.description,
metadata: args.metadata ?? existing.metadata,
inputSchema: args.inputSchema !== undefined ? args.inputSchema : existing.inputSchema,
groundTruthSchema: args.groundTruthSchema !== undefined ? args.groundTruthSchema : existing.groundTruthSchema,
updatedAt: new Date(),
};
} as DatasetRecord;
this.db.datasets.set(args.id, updated);
return updated;
return toDatasetRecord(updated);
}

async deleteDataset({ id }: { id: string }): Promise<void> {
Expand Down Expand Up @@ -124,7 +140,7 @@ export class DatasetsInMemory extends DatasetsStorage {
const end = perPageInput === false ? datasets.length : start + perPage;

return {
datasets: datasets.slice(start, end),
datasets: datasets.slice(start, end).map(toDatasetRecord),
pagination: {
total: datasets.length,
page,
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1907,17 +1907,17 @@ export interface CreateDatasetInput {
name: string;
description?: string;
metadata?: Record<string, unknown>;
inputSchema?: Record<string, unknown>;
groundTruthSchema?: Record<string, unknown>;
inputSchema?: Record<string, unknown> | null;
groundTruthSchema?: Record<string, unknown> | null;
}

export interface UpdateDatasetInput {
id: string;
name?: string;
description?: string;
metadata?: Record<string, unknown>;
inputSchema?: Record<string, unknown>;
groundTruthSchema?: Record<string, unknown>;
inputSchema?: Record<string, unknown> | null;
groundTruthSchema?: Record<string, unknown> | null;
}

export interface AddDatasetItemInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ export function EditDatasetDialog({ open, onOpenChange, dataset, onSuccess }: Ed
const [validationError, setValidationError] = useState<string | null>(null);
const { updateDataset } = useDatasetMutations();

// Sync form state when dataset prop changes
// Sync form state when dialog opens
useEffect(() => {
setName(dataset.name);
setDescription(dataset.description ?? '');
setInputSchema(dataset.inputSchema ?? null);
setGroundTruthSchema(dataset.groundTruthSchema ?? null);
setValidationError(null);
}, [dataset.name, dataset.description, dataset.inputSchema, dataset.groundTruthSchema]);
if (open) {
setName(dataset.name);
setDescription(dataset.description ?? '');
setInputSchema(dataset.inputSchema ?? null);
setGroundTruthSchema(dataset.groundTruthSchema ?? null);
setValidationError(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);

const handleSchemaChange = (schemas: {
inputSchema: Record<string, unknown> | null;
Expand Down Expand Up @@ -74,11 +77,13 @@ export function EditDatasetDialog({ open, onOpenChange, dataset, onSuccess }: Ed
onSuccess?.();
} catch (err: unknown) {
// Handle validation errors (existing items may fail new schema)
const error = err as { cause?: { failingItems?: unknown[] }; message?: string };
if (error?.cause?.failingItems) {
const count = error.cause.failingItems.length;
// MastraClientError stores the parsed response body in `body`
const body = (err as { body?: { cause?: { failingItems?: unknown[] } } })?.body;
if (Array.isArray(body?.cause?.failingItems) && body.cause.failingItems.length > 0) {
const count = body.cause.failingItems.length;
setValidationError(`${count} existing item(s) fail validation. Fix items or adjust schema.`);
} else {
const error = err as { message?: string };
toast.error(`Failed to update dataset: ${error?.message || 'Unknown error'}`);
}
Comment on lines 81 to 88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Defensive guard missing for empty failingItems array

body?.cause?.failingItems evaluates to truthy even when it is an empty array ([]), which would display the misleading message "0 existing item(s) fail validation. Fix items or adjust schema." — although the server is unlikely to send an empty array here, a length guard prevents the confusion entirely.

🛡️ Proposed defensive fix
-      const body = (err as { body?: { cause?: { failingItems?: unknown[] } } })?.body;
-      if (body?.cause?.failingItems) {
-        const count = body.cause.failingItems.length;
+      const body = (err as { body?: { cause?: { failingItems?: unknown[] } } })?.body;
+      const failingItems = body?.cause?.failingItems;
+      if (Array.isArray(failingItems) && failingItems.length > 0) {
+        const count = failingItems.length;
         setValidationError(`${count} existing item(s) fail validation. Fix items or adjust schema.`);
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/playground-ui/src/domains/datasets/components/edit-dataset-dialog.tsx`
around lines 81 - 88, The current check treats an empty failingItems array as
truthy and can show "0 existing item(s) fail validation"; update the guard in
the error handling for body (the variable `body` derived from `err`) to verify
failingItems is a non-empty array (e.g., check
Array.isArray(body.cause.failingItems) && body.cause.failingItems.length > 0)
before calling setValidationError, otherwise fall back to the toast.error path
(using `toast.error` and the `error?.message` fallback).

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ export function SchemaField({
const [parseError, setParseError] = useState<string | null>(null);
// Track if we've already auto-populated to avoid repeated population on re-enable
const hasAutoPopulatedRef = useRef(false);
// Track whether the latest value change originated from local editing
const isLocalEditRef = useRef(false);

// Sync jsonText when value changes from outside (e.g., import)
useEffect(() => {
if (isLocalEditRef.current) {
isLocalEditRef.current = false;
return;
}
if (value) {
setJsonText(JSON.stringify(value, null, 2));
setParseError(null);
Expand Down Expand Up @@ -74,6 +80,7 @@ export function SchemaField({
const parsed = JSON.parse(text);
if (typeof parsed === 'object' && parsed !== null) {
setParseError(null);
isLocalEditRef.current = true;
onChange(parsed);
} else {
setParseError('Schema must be a JSON object');
Expand Down
2 changes: 2 additions & 0 deletions packages/playground/src/pages/datasets/dataset/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ function DatasetPage() {
id: dataset.id,
name: dataset.name,
description: dataset?.description || '',
inputSchema: dataset.inputSchema,
groundTruthSchema: dataset.groundTruthSchema,
}}
/>
)}
Expand Down
8 changes: 4 additions & 4 deletions packages/server/src/server/handlers/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export const CREATE_DATASET_ROUTE = createRoute({
name,
description,
metadata,
inputSchema: inputSchema ?? undefined,
groundTruthSchema: groundTruthSchema ?? undefined,
inputSchema,
groundTruthSchema,
});
const details = await ds.getDetails();
return details as any;
Expand Down Expand Up @@ -201,8 +201,8 @@ export const UPDATE_DATASET_ROUTE = createRoute({
name,
description,
metadata,
inputSchema: inputSchema ?? undefined,
groundTruthSchema: groundTruthSchema ?? undefined,
inputSchema,
groundTruthSchema,
});
return result as any;
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/server/schemas/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ export const datasetResponseSchema = z.object({
name: z.string(),
description: z.string().optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
inputSchema: z.record(z.unknown()).optional().nullable(),
groundTruthSchema: z.record(z.unknown()).optional().nullable(),
inputSchema: z.record(z.unknown()).optional(),
groundTruthSchema: z.record(z.unknown()).optional(),
version: z.number().int(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
Expand Down
23 changes: 22 additions & 1 deletion server-adapters/hono/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,28 @@ export class MastraServer extends MastraServerBase<HonoApp, HonoRequest, Context
// Check for direct status property (HTTPException)
if ('status' in error) {
const status = (error as any).status;
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, status);
let safeCause: { failingItems: unknown[] } | undefined;
try {
const raw = error instanceof Error ? error.cause : undefined;
if (
raw &&
typeof raw === 'object' &&
!Array.isArray(raw) &&
'failingItems' in raw &&
Array.isArray((raw as any).failingItems)
) {
safeCause = { failingItems: (raw as any).failingItems };
}
} catch {
// serialization or access error — omit cause
}
return c.json(
{
error: error instanceof Error ? error.message : 'Unknown error',
...(safeCause ? { cause: safeCause } : {}),
},
status,
);
}
// Check for MastraError with status in details
if ('details' in error && error.details && typeof error.details === 'object' && 'status' in error.details) {
Expand Down
9 changes: 5 additions & 4 deletions stores/libsql/src/storage/domains/datasets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ export class DatasetsLibSQL extends DatasetsStorage {
name: input.name,
description: input.description,
metadata: input.metadata,
inputSchema: input.inputSchema,
groundTruthSchema: input.groundTruthSchema,
inputSchema: input.inputSchema ?? undefined,
groundTruthSchema: input.groundTruthSchema ?? undefined,
version: 0,
createdAt: now,
updatedAt: now,
Expand Down Expand Up @@ -258,8 +258,9 @@ export class DatasetsLibSQL extends DatasetsStorage {
name: args.name ?? existing.name,
description: args.description ?? existing.description,
metadata: args.metadata ?? existing.metadata,
inputSchema: args.inputSchema !== undefined ? args.inputSchema : existing.inputSchema,
groundTruthSchema: args.groundTruthSchema !== undefined ? args.groundTruthSchema : existing.groundTruthSchema,
inputSchema: (args.inputSchema !== undefined ? args.inputSchema : existing.inputSchema) ?? undefined,
groundTruthSchema:
(args.groundTruthSchema !== undefined ? args.groundTruthSchema : existing.groundTruthSchema) ?? undefined,
updatedAt: new Date(now),
};
} catch (error) {
Expand Down
9 changes: 5 additions & 4 deletions stores/pg/src/storage/domains/datasets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ export class DatasetsPG extends DatasetsStorage {
name: input.name,
description: input.description,
metadata: input.metadata,
inputSchema: input.inputSchema,
groundTruthSchema: input.groundTruthSchema,
inputSchema: input.inputSchema ?? undefined,
groundTruthSchema: input.groundTruthSchema ?? undefined,
version: 0,
createdAt: now,
updatedAt: now,
Expand Down Expand Up @@ -305,8 +305,9 @@ export class DatasetsPG extends DatasetsStorage {
name: args.name ?? existing.name,
description: args.description ?? existing.description,
metadata: args.metadata ?? existing.metadata,
inputSchema: args.inputSchema !== undefined ? args.inputSchema : existing.inputSchema,
groundTruthSchema: args.groundTruthSchema !== undefined ? args.groundTruthSchema : existing.groundTruthSchema,
inputSchema: (args.inputSchema !== undefined ? args.inputSchema : existing.inputSchema) ?? undefined,
groundTruthSchema:
(args.groundTruthSchema !== undefined ? args.groundTruthSchema : existing.groundTruthSchema) ?? undefined,
updatedAt: new Date(now),
};
} catch (error) {
Expand Down
Loading