tooltipdev/lexicon-to-zod(新しいタブで開きます)
主要言語: TypeScript
Convert ATProtocol Lexicons to Zod schemas
- star 数
- 6
- watcher 数
- 1
- fork 数
- 0
- issue 数
- 0
- 最終更新
- 2025/05/10
主要言語: TypeScript
Convert ATProtocol Lexicons to Zod schemas
Runtime validation for ATProtocol Lexicons 🔎📜
lexicon-to-zod is a fully extensible framework for generating Zod schemas for validating ATProtocol Lexicon JSON.
Review types and code for information beyond this README.
npm i lexicon-to-zod
You can generate a map of Zod schemas for a full Lexicon document via the exported lexiconToZod method. Each entry in the defs dictionary will be converted to a Zod schema.
import { lexiconToZod } from "lexicon-to-zod";
const lexicon = {
"lexicon": 1,
"id": "com.atproto.some.cool.lexicon",
"defs": {
"main": {
"type": "procedure",
"input": {...},
"output": {...},
},
"someOtherDef": {...}
}
};
const schemaMap = lexiconToZod(lexicon);
{
"lexicon": 1,
"id": "com.atproto.some.cool.lexicon",
"defs": {
"main": {
"type": "procedure",{...},
"input": {
"encoding: ...,
"schema": {...}
},
"output": {
"encoding: ...,
"schema": {...}
},
},
"someOtherDef": {
"type": "object",
"properties": {...}
}
}
}
{
defs: {
main: {
input: {
encoding: z.literal(...),
schema: z.object({...})
},
output: {
encoding: z.literal(...),
schema: z.object({...})
},
},
someOtherDef: z.object({...})
}
}
Each schema can be used to validate Lexicon JSON data. Review Zod documentation for more information on schema usage and customization.
import { lexiconToZod } from "lexicon-to-zod";
const lexicon = {
"lexicon": 1,
"id": "com.atproto.some.cool.lexicon",
"defs": {
"main": {
"type": "procedure",
"input": {...},
"output": {...},
},
"user": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
}
}
}
};
const schemaMap = lexiconToZod(lexicon);
const userValidator = schemaMap.defs.user;
let results = userValidator.parse({
"name": "John",
"email": "john.username@gmail.com"
})
// prints {"name": "John", "email": "john.username@gmail.com"}
console.log(results)
// throws due to invalid type
results = userValidator.parse({
"name": 123, // invalid type
"email": "john.username@gmail.com"
});
/**
* Or using safeParse.
*/
let safeParseResults = userValidator.safeParse({
"name": "John",
"email": "john.username@gmail.com"
})
// prints {"success": true, "data": {...}}
console.log(safeParseResults);
safeParseResults = userValidator.safeParse({
"name": 123, // invalid type
"email": "john.username@gmail.com"
})
// prints {"success": false, "error": ...}
console.log(safeParseResults);
You can generate Zod schemas for subsections of Lexicon documents by utilizing the built-in type parsers. The output will be an individual Zod schema instead of a map of Zod schemas.
import { parsers } from "lexicon-to-zod";
const { object: objectParser } = parsers();
const lexiconPartial = {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
};
const schema = objectParser(lexiconPartial);
const results = schema.parse({ "foo": "bar" })
{
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
z.object({
foo: z.string()
})
Schema generation can be modified via LexiconToZodOptions. These options will be mentioned further in this README.
The option type definition should be used as a reference.
Some Lexicon types contain references to other Lexicon definitions, for example ref values and union.refs values. In these cases, referenced Lexicons need to be gathered to fulfil schema generation, otherwise a placeholder schema will be used.
followRefs option to true.lexiconDict option for Lexicon reference lookup if followRefs is set to true.lexiconDict must be in the format {[NSID]: FullLexiconDocument}.import { lexiconToZod } from "lexicon-to-zod";
const lexiconDict = {
"com.atproto.some.cool.lexicon": {
main: {...},
"someOtherDef": {
"type": "ref",
"ref": "lex:com.atproto.some.other.lexicon#someCoolDef"
}
},
"com.atproto.some.other.lexicon": {
main: {...},
"someCoolDef": {...}
},
};
const lexicon = lexiconDict["com.atproto.some.cool.lexicon"];
// Lexicon refs will resolve to proper Zod schemas.
const schemaMap = lexiconToZod(lexicon, {
followRefs: true
lexiconDict,
});
If your Lexicon contains ref types you must set the followRefs option to true if you want them converted to Zod schemas. If the followRefs option is omitted, ref field types will be converted to z.any().
If your Lexicon contains union types you must set the followRefs option to true if you want its subtypes converted to Zod schemas. If the followRefs option is omitted, union subtype schemas will be converted to z.any().
If the Lexicon graph you are trying to convert contains circular references your code will throw errors. You will need to manage the circular reference with additional logic.
z.lazy.You can pass the @atproto/api Lexicon map as lexiconDict to handle most standard behavior.
npm i @atproto/api
import { lexiconToZod } from "lexicon-to-zod";
// Import Lexicon dictionary.
import { schemaDict } from "@atproto/api/dist/client/lexicons";
// Convert dictionary keys to NSID format.
const lexiconDict: Record<string, any> =
Object.values(schemaDict).reduce((acc, l) => ({[l.id]: l, ...acc}), {});
const lexicon = lexiconDict["app.bsky.feed.post"];
const schemaMap = lexiconToZod(lexicon, { lexiconDict });
Each supported Lexicon type will be converted to a matching Zod schema type using a type specific type parser. You can utilize the typeParserDict option to override a type parser, or add an unsupported type parser.
import { lexiconToZod } from "lexicon-to-zod";
const lexicon = ...;
const typeParserDict = {
// will be used to generate schemas for `string` Lexicon fields
string: () => {},
// will be used to generate schemas for `someCustomType` Lexicon fields
someCustomType: () => {}
};
const schemaMap = lexiconToZod(lexicon, { typeParserDict });
typeParserDict should be of type TypeParserMap. Provided type parsers will have priority over the built-in parser dictionary.
Type parser selection is based off of a Lexicon fields type value, so any unsupported Lexicon types can be added to typeParserDict. When the type is encountered during schema generation your custom type parser will be invoked.
If no parser can be matched to a Lexicon type value the $default type parser will be used. The included $default parser will simply return z.any().
You can override $default via the typeParserDict option to provide your own custom implementation.
pathOptions is an optional map that provides more granular control over Zod schema generation at the individual Lexicon property/field level. It allows for functionality such as omitting Lexicon fields from schema input/output, or overriding schema generation for a single property with your own Zod schema.
PathOptions.lexiconToZod produces a map of schemas, one schema for each def (main, etc).
pathOption keys should include the leading path (ie main.input.schema, someDef.someProp).pathOptions keys should include no leading path (ie someProp).import { lexiconToZod, parsers } from "lexicon-to-zod";
const lexicon = {
"lexicon": 1,
"id": "com.atproto.some.cool.lexicon",
"defs": {
"main": {
"type": "record",
"record": {
"type": "object",
"properties: {...}
}
},
"someDef": {
"type": "object",
"properties: {...}
}
}
};
const schemaMap = lexiconToZod(lexicon, {
pathOptions: {
"main.record.someProp: {
"metadata": {...}, // arbitrary metadata attached to output schema
"additionalProps": {...}, // additional properties for output 'object' schemas,
},
"main.record.someOtherProp: {
"isOptional": true // apply ".isOptional()" to field
},
"someOtherDef.someProp: {
"override": z.string() // override output schema
},
"someOtherDef.someOtherProp: {
"override": null // omit Lexicon field from output schema
}
}
});
const schema = parsers().object(lexicon.defs.someDef, {
"someProp: {
"override": z.string() // override output schema
},
"someOtherProp: {
"override": null // omit Lexicon field from output schema
}
});
You must use special path structures for certain types and subtypes when writing dot-notated paths for pathOptions .
| Target field type | Path structure | Description |
|---|---|---|
| Array Elements | somePath.__array__ |
Target Array element schema |
| Union Subtypes | somePath.__union__.0-n |
Target Union subtype schema at index {0-n} |
All contributions are appreciated. Fork the repo and open a pull request.
src/types.ts file for a better understanding of options.typeParserDict.typeParserDict.pathOptions.shadcn has form validation via react-hook-form. Think about creative ways of UI and code generation via schemas.