frontend-plugin-api: remove deprecated createSchemaFromZod

The helper is no longer used internally and has been replaced by
the `configSchema` option with direct Standard Schema values.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-10 11:18:03 +02:00
parent 1e6d2e91ad
commit 2d89e198bd
8 changed files with 14 additions and 131 deletions
@@ -1,5 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/frontend-plugin-api': minor
---
Refactored the internal `createSchemaFromZod` helper to use a schema-first generic pattern, replacing the `ZodSchema<TOutput, ZodTypeDef, TInput>` constraint with `TSchema extends ZodType`. This avoids "excessively deep" type inference errors when multiple Zod copies are resolved.
**BREAKING**: Removed the deprecated `createSchemaFromZod` helper. Use the `configSchema` option with Standard Schema values instead, for example `configSchema: { title: z.string() }` using zod v3.25+ or v4.
@@ -1,43 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSchemaFromZod } from './createSchemaFromZod';
describe('createSchemaFromZod', () => {
it('should provide nice parse errors', () => {
const { parse } = createSchemaFromZod(z =>
z.object({
foo: z.union([z.string(), z.number()]),
derp: z.object({ bar: z.number() }),
}),
);
expect(() => {
// @ts-expect-error
return parse({ derp: { bar: 'derp' } });
}).toThrow(
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
);
expect(() => {
// @ts-expect-error
return parse(undefined);
}).toThrow(`Missing required value`);
expect(() => {
// @ts-expect-error
return parse('derp');
}).toThrow(`Expected object, received string`);
});
});
@@ -1,74 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { z, type ZodIssue, type ZodType } from 'zod/v3';
import zodToJsonSchema from 'zod-to-json-schema';
import { PortableSchema } from './types';
/**
* @internal
* @deprecated Use {@link createConfigSchema} instead.
*/
export function createSchemaFromZod<TSchema extends ZodType>(
schemaCreator: (zImpl: typeof z) => TSchema,
): PortableSchema<z.output<TSchema>, z.input<TSchema>> {
const schema = schemaCreator(z);
let cached: PortableSchema['schema'] | undefined;
const result: PortableSchema<z.output<TSchema>, z.input<TSchema>> = {
parse: input => {
const parseResult = schema.safeParse(input);
if (parseResult.success) {
return parseResult.data;
}
throw new Error(parseResult.error.issues.map(formatIssue).join('; '));
},
schema: undefined as any,
};
Object.defineProperty(result, 'schema', {
get() {
if (!cached) {
const jsonSchema = zodToJsonSchema(schema) as JsonObject;
cached = Object.assign(
() => ({ schema: jsonSchema }),
jsonSchema,
) as PortableSchema['schema'];
}
return cached;
},
configurable: true,
enumerable: true,
});
return result;
}
function formatIssue(issue: ZodIssue): string {
if (issue.code === 'invalid_union') {
return formatIssue(issue.unionErrors[0].issues[0]);
}
let message = issue.message;
if (message === 'Required') {
message = `Missing required value`;
}
if (issue.path.length) {
message += ` at '${issue.path.join('.')}'`;
}
return message;
}
+4 -4
View File
@@ -343,9 +343,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
@@ -413,9 +413,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -2
View File
@@ -334,9 +334,9 @@ export const EntityContentBlueprint: ExtensionBlueprint<{
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -2
View File
@@ -808,9 +808,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -2
View File
@@ -101,9 +101,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};
+2 -2
View File
@@ -132,9 +132,9 @@ const _default: OverridableFrontendPlugin<
icon: string | undefined;
};
configInput: {
filter?: FilterPredicate | undefined;
title?: string | undefined;
path?: string | undefined;
title?: string | undefined;
filter?: FilterPredicate | undefined;
group?: string | false | undefined;
icon?: string | undefined;
};