Skip to main content

Validation plugin

@merkur/plugin-validation is a Merkur plugin for schema-based validation. It provides library-agnostic validation for widget props that works with any schema library implementing the safeParse() interface, including:

Installation

npm install @merkur/plugin-validation @esmj/schema

Requirements

This plugin requires @merkur/plugin-component to be installed and registered before it.

Basic Usage

With @esmj/schema

import { s } from '@esmj/schema';
import { defineWidget } from '@merkur/core';
import { componentPlugin } from '@merkur/plugin-component';
import { validationPlugin } from '@merkur/plugin-validation';

const propsSchema = s.object({
userId: s.string(),
count: s.number().optional(),
config: s.object({
theme: s.enum(['light', 'dark']),
}),
});

export default defineWidget({
name: 'my-widget',
version: '1.0.0',
$plugins: [
componentPlugin,
validationPlugin({
props: propsSchema,
}),
],
// ... widget implementation
});

With zod

import { z } from 'zod';
import { defineWidget } from '@merkur/core';
import { componentPlugin } from '@merkur/plugin-component';
import { validationPlugin } from '@merkur/plugin-validation';

const propsSchema = z.object({
userId: z.string(),
count: z.number().optional(),
config: z.object({
theme: z.enum(['light', 'dark']),
}),
});

export default defineWidget({
name: 'my-widget',
version: '1.0.0',
$plugins: [
componentPlugin,
validationPlugin({
props: propsSchema,
}),
],
// ... widget implementation
});

For detailed benchmarks and feature comparisons, see the @esmj/schema documentation on npm.

note

If you're already using zod in your project, you can continue using it with @merkur/plugin-validation. The plugin is library-agnostic and works with any schema library that implements the safeParse() method. But @esmj/schema is recommended.

Options

props (required)

The schema object used for props validation. Must implement:

  • safeParse(value) - Returns { success: boolean, data?, error? }

onError (optional)

Custom error handler function. Default: null (throws validation error)

ValueDescription
nullThrows the validation error (default)
functionCustom handler (widget, result) => void

Custom error handler example

validationPlugin({
props: propsSchema,
onError: (widget, result) => {
// Send to error tracking service
errorTracker.captureException(result.error, {
widget: widget.name,
});

// Or log with custom formatting
console.warn(`Widget ${widget.name} received invalid props:`, result.error);
},
});

When Validation Runs

The plugin validates props at two points:

  1. On mount - Initial props are validated when the widget mounts
  2. On setProps - Props are validated each time widget.setProps() is called

Validation is run against the merged props (existing props + new props), ensuring the complete props object is valid.

On successful validation, the result.data (transformed/coerced values) replaces the original props. This means schemas can also perform data transformation, not just validation.

Custom Element Integration

When using with @merkur/integration-custom-element, the validation plugin provides a cleaner approach for parsing HTML attributes to widget props. Instead of defining individual attributesParser functions, you can define a single schema that handles both validation and type coercion.

The @esmj/schema library supports type coercion, which automatically converts string attributes to the correct types:

import { registerCustomElement } from '@merkur/integration-custom-element';
import { componentPlugin } from '@merkur/plugin-component';
import { validationPlugin } from '@merkur/plugin-validation';
import { s } from '@esmj/schema';

// Schema with coercion - automatically converts strings to correct types
const propsSchema = s.object({
title: s.string(),
theme: s.string(),
count: s.cast.number(), // Casts "42" → 42
enabled: s.cast.boolean(), // Casts "true" → true
config: s.cast.json(s.object({ apiUrl: s.string() })), // Parses JSON strings automatically
});

const widgetDefinition = {
name: 'my-widget',
version: '1.0.0',
$plugins: [
componentPlugin,
validationPlugin({ props: propsSchema }),
],
// ... widget implementation
};

registerCustomElement({
widgetDefinition,
observedAttributes: ['title', 'theme', 'count', 'enabled', 'config'],
});
<my-widget 
title="Hello World"
theme="dark"
count="42"
enabled="true"
config='{"apiUrl": "https://api.example.com"}'
></my-widget>

Benefits over attributesParser

FeatureattributesParservalidationPlugin + schema
Type coercionManual per attributeAutomatic via schema
ValidationNone built-inFull validation with errors
Type safetyNoYes (with TypeScript)
ReusabilityLimitedSchema can be reused
Error handlingManualConfigurable via onError
Default valuesNot supportedVia schema defaults

TypeScript

Full TypeScript support is included:

 import { defineWidget } from '@merkur/core';
import { componentPlugin } from '@merkur/plugin-component';
import { validationPlugin, type ValidationPluginOptions } from '@merkur/plugin-validation';
import { s, type Infer } from '@esmj/schema';

const propsSchema = s.object({
userId: s.string(),
count: s.number().optional(),
});

type Props = Infer<typeof propsSchema>;

const options: ValidationPluginOptions<Props> = {
props: propsSchema,
};

export default defineWidget({
$plugins: [
componentPlugin,
validationPlugin(options),
],
});