---
title: "Schema validation plugin"
description: "Validate Markdown, JSON, and YAML documents against a source JSON Schema."
canonical_url: "https://content.comark.dev/plugins/built-in/schema-validation"
---
# Schema validation plugin

> Validate Markdown, JSON, and YAML documents against a source JSON Schema.

When a [source](https://content.comark.dev/sources) declares a [`schema`](https://content.comark.dev/sources/custom#interface-schema), the `schema-validation` plugin checks every Markdown, JSON, and YAML document on the [`file:parsed`](https://content.comark.dev/reference/content/hooks#fileparsed) hook: on load, on watch, and on [`content.ingest()`](https://content.comark.dev/reference/content/ingest). How failures are handled depends on [`onError`](#api-onerror).

[`content.update()`](https://content.comark.dev/reference/content/update) stores its input as already parsed and does not validate it — the file is expected to have been validated by whichever instance parsed it. Use `ingest()` for a file you built yourself.

Register the plugin explicitly. Skip validation for a whole instance or individual paths with the [`ignore`](#api-ignore) option.

## Usage

```ts [content.ts]
import { comarkContent, type JsonSchema } from 'comark-content'
import fs from 'comark-content/sources/fs'
import json from 'comark-content/plugins/json'
import yaml from 'comark-content/plugins/yaml'
import schemaValidation from 'comark-content/plugins/schema-validation'

const schema: JsonSchema = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    author: { type: 'string' },
    tags: { type: 'array', items: { type: 'string' } },
    seo: {
      type: 'object',
      properties: { title: { type: 'string' } },
      required: ['title'],
    },
  },
  required: ['title', 'author'],
}

const content = comarkContent({
  source: fs('./content', { schema }),
  plugins: [json(), yaml(), schemaValidation()],
})
```

Given a content folder with valid and invalid files:

```md [valid-post.md]
---
title: Valid post
author: Ada Lovelace
---
# Valid post
```

```json [valid-settings.json]
{
  "title": "Site settings",
  "author": "Ada Lovelace"
}
```

```md [missing-author.md]
---
title: Missing author
---
# This file is excluded — `author` is required
```

```json [bad-types.json]
{
  "title": 42,
  "author": "Ada Lovelace"
}
```

```yaml [incomplete-seo.yaml]
title: Incomplete SEO
author: Ada Lovelace
seo: {}
```

```ts [load.ts]
await content.init()

content.stat('/valid-post') // loaded
content.stat('/valid-settings') // loaded
content.stat('/missing-author') // undefined — rejected
content.stat('/bad-types') // undefined — rejected
content.stat('/incomplete-seo') // undefined — rejected
```

Invalid documents never enter the manifest. With `onError: 'throw'`, load stops on the first invalid file.

See the [Vite + Vue schema validation example](https://content.comark.dev/examples/vite/vue-schema-validation) for a runnable demo.

---

## API

### `schemaValidation(options?)`{lang="ts"}

Returns a [`ContentPlugin`](https://content.comark.dev/reference/types/plugins#contentplugin) that hooks [`file:parsed`](https://content.comark.dev/reference/content/hooks#fileparsed).

**Parameters:**

| Name                      | Type                            | Default           | Description                                                                         |
| ------------------------- | ------------------------------- | ----------------- | ----------------------------------------------------------------------------------- |
| [`onError`](#api-onerror) | `'throw' \| 'warn' \| 'ignore'` | Content `onError` | How validation failures are handled on load, watch, and `ingest()`.                 |
| `ignore`                  | `Iterable<string>`              | `undefined`       | Instance names or document paths to skip validation for. Accepts an array or `Set`. |

**Returns:** [`ContentPlugin`](https://content.comark.dev/reference/types/plugins#contentplugin)

```ts
import schemaValidation from 'comark-content/plugins/schema-validation'

const content = comarkContent({
  plugins: [
    schemaValidation({
      onError: 'throw',
      ignore: new Set(['drafts', 'default/bad-types.json']),
    }),
  ],
})
```

### `onError`

Each mode reacts differently to an invalid file:

| Value    | Behavior                                                                                      |
| -------- | --------------------------------------------------------------------------------------------- |
| `throw`  | Throw — stops load, watch handling, or `ingest()` on the first invalid file.                  |
| `warn`   | Log a warning and exclude the file (`ctx.file = null`). On `ingest()`, the write is rejected. |
| `ignore` | Silently exclude the file. On `ingest()`, the write is rejected.                              |

Defaults to the Content-level [`onError`](https://content.comark.dev/reference/content/comark-content#options-onerror) (which defaults to `'warn'`).

### `ignore`

Each entry is one of:

| Form            | Example                                     | Skips                                                                                                                                                              |
| --------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Instance name   | `'drafts'`                                  | Every document in the `drafts` instance                                                                                                                            |
| Warning id      | `'default/bad-types.json'`                  | One file, by its [`meta.key`](https://content.comark.dev/guide/files-and-paths#internal-identifiers) (`<instance name>/<file key>`), as printed in validation logs |
| Pasted log line | `'├ default/bad-types.json: Expected …'`    | Normalized to the warning id above                                                                                                                                 |
| Public path     | `'/about'`                                  | The document at that path                                                                                                                                          |
| Glob            | `'default/legacy/*.json'`, `'**/drafts/**'` | Picomatch against warning ids and keys                                                                                                                             |

Copy straight from a validation warning — the `<instance name>/<file key>` before the colon:

```
├ default/bad-types.json: Expected string, received integer for property "title".
  ^^^^^^^^^^^^^^^^^^^^^^ paste this
```

```ts [content.ts]
const content = comarkContent({
  source: fs('./content', { schema }),
  plugins: [
    schemaValidation({
      ignore: [
        'default/bad-types.json',
        'drafts',
        'default/legacy/*.json',
      ],
    }),
  ],
})
```

The source [`schema`](https://content.comark.dev/sources/custom#interface-schema) is still used for types and query columns on ignored entries.

```ts [content.ts]
// Drafts share the schema for types and query columns, but skip validation.
export const drafts = comarkContent('drafts', {
  source: fs('./drafts', { schema }),
  plugins: [schemaValidation({ ignore: ['drafts'] })],
})
```

---

## How it works

On [`file:parsed`](https://content.comark.dev/reference/content/hooks#fileparsed):

1. Collect validation issues against `source.schema` (skipped when the file matches `ignore`).
2. Apply [`onError`](#api-onerror) — throw, warn and exclude, or silently exclude.

Invalid files log one line per issue:

```
├ default/bad-types.json: Expected string, received integer for property "title".
```

[Snapshot hydration](https://content.comark.dev/advanced/artifacts-and-hydration) and [manifest hydration](https://content.comark.dev/advanced/artifacts-and-hydration) skip `file:parsed` entirely — artifacts are checksum-verified exports loaded as-is.

Register [`markdown-fields`](https://content.comark.dev/plugins/built-in/markdown-fields) **before** this plugin so markdown field transforms run before validation.

---

## Validated extensions

`.md`, `.markdown`, `.json`, `.yaml`, `.yml`

Media and other binary entries are skipped. Parsed [`MarkdownDocument`](https://content.comark.dev/plugins/built-in/markdown-fields) values are accepted for `x-content: { type: 'markdown' }` fields.


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
