Schema validation and TS types for LIP-2 Lens Protocol Metadata Standards.
- Features
- Installation
- Documentation
- Types
- JSON schemas
- Versioning
- Contributing
- Releasing
- License
- Support
- Zod schema definitions
- JSON Schema definitions
- TypeScript type definitions
# npm:
npm install @lens-protocol/metadata zod
# yarn:
yarn add @lens-protocol/metadata zod
# pnpm:
pnpm add @lens-protocol/metadata zodNote
zod is marked as optional peer dependency, so if you all you need is the JSON Schema definitions, you can install @lens-protocol/metadata without zod.
See https://lens-protocol.github.io/metadata/.
You can create compliant PostMetadata objects via the following builder functions:
import {
article,
audio,
checkingIn,
embed,
event,
image,
link,
livestream,
mint,
space,
story,
textOnly,
threeD,
transaction,
video,
} from '@lens-protocol/metadata';
const json = article({
content: 'The content of the article',
});Note
Use the type definitions to explore the available properties and their types. The builders will throw a ValidationError with instructions on how to fix the error if the object is not compliant with the schema.
We also provided a set of builder function for specific metadata sub-types (list to be expanded):
import { geoUri } from '@lens-protocol/metadata';
const uri = geoUri({
lat: 51.5074,
lng: 0.1278,
});You can create compliant AccountMetadata objects via the following builder function:
import { account } from '@lens-protocol/metadata';
const json = account({
name: 'Bob',
bio: 'I am a Lens user',
});Note
Use the type definitions to explore the available properties and their types. The builder will throw a ValidationError with instructions on how to fix the error if the object is not compliant with the schema.
Assuming we have 2 JS objects:
const valid = {
/** example of valid metadata **/
};
const invalid = {
/** example of invalid metadata **/
};Post metadata schema is a union of all content schemas (e.g. ArticleMetadata, AudioMetadata, etc.
Use it to parse the metadata referenced by contentURI of Lens Post.
import { PostMetadataSchema } from '@lens-protocol/metadata';
PostMetadataSchema.parse(valid); // => PostMetadata
PostMetadataSchema.parse(invalid); // => throws ZodError
// OR
PostMetadataSchema.safeParse(valid);
// => { success: true, data: PostMetadata }
PostMetadataSchema.safeParse(invalid);
// => { success: false, error: ZodError }import { AccountMetadataSchema } from '@lens-protocol/metadata';
AccountMetadataSchema.parse(valid); // => AccountMetadata
AccountMetadataSchema.parse(invalid); // => throws ZodError
// OR
AccountMetadataSchema.safeParse(valid);
// => { success: true, data: AccountMetadata }
AccountMetadataSchema.safeParse(invalid);
// => { success: false, error: ZodError }A convenience extractVersion function is available to extract the version from a parsed PublicationMetadata or ProfileMetadata.
import { extractVersion, PostMetadataSchema, AccountMetadataSchema } from '@lens-protocol/metadata';
const postMetadata = PostMetadataSchema.parse(valid);
extractVersion(postMetadata); // => '3.0.0'
const accountMetadata = AccountMetadataSchema.parse(valid);
extractVersion(accountMetadata); // => '1.0.0'ZodError contains all the information needed to inform you about the validation error, but it's not very user friendly. You can use formatZodError to get a more readable error message.
import { PostMetadataSchema, formatZodError } from '@lens-protocol/metadata';
const result = PostMetadataSchema.safeParse(invalid);
if (!result.success) {
console.log(formatZodError(result.error));
}Every time you have a discriminated union, you can use the discriminant to narrow the type. See few examples below.
import { PostMetadata, PostMetadataSchema, PostSchemaId } from '@lens-protocol/metadata';
const metadata = PostMetadataSchema.parse(valid);
switch (metadata.$schema) {
case PostSchemaId.ARTICLE_LATEST:
// metadata is ArticleMetadata
break;
case PostSchemaId.AUDIO_LATEST:
// metadata is AudioMetadata
break;
case PostSchemaId.IMAGE_LATEST:
// metadata is ImageMetadata
break;
case PostSchemaId.TEXT_ONLY_LATEST:
// metadata is TextOnlyMetadata
break;
// ...
}import { AccessCondition, ConditionType, PostMetadataSchema } from '@lens-protocol/metadata';
const metadata = PostMetadataSchema.parse(valid);
switch (metadata.encryptedWith?.accessCondition.type) {
case ConditionType.AND:
// accessCondition is AndCondition
break;
case ConditionType.OR:
// accessCondition is OrCondition
break;
case ConditionType.NFT_OWNERSHIP:
// accessCondition is NftOwnershipCondition
break;
case ConditionType.EOA_OWNERSHIP:
// accessCondition is EoaOwnershipCondition
break;
// ...
}import { MetadataAttribute, MetadataAttributeType } from '@lens-protocol/metadata';
switch (attribute.type) {
case MetadataAttributeType.BOOLEAN:
// attribute is BooleanAttribute
// value is a string "true" or "false"
break;
case MetadataAttributeType.DATE:
// attribute is DateAttribute
// value is a string in ISO 8601 format
break;
case MetadataAttributeType.NUMBER:
// attribute is NumberAttribute
// value is a string containing a valid JS number
break;
case MetadataAttributeType.STRING:
// attribute is StringAttribute
// value is a string
break;
case MetadataAttributeType.JSON:
// attribute is JSONAttribute
// value is a string allegedly containing a valid JSON, consumers should validate it
break;
}The package also exports all enums and types that you might need to work with the metadata.
Use your IDE's autocomplete to explore the available types.
Some examples:
import {
// enums
MediaAudioKind,
MediaAudioMimeType,
MediaImageMimeType,
MediaVideoMimeType,
MetadataAttributeType,
PostMainFocus,
ThreeDFormat,
// main types
ArticleMetadata,
AudioMetadata,
CheckingInMetadata,
EmbedMetadata,
EventMetadata,
ImageMetadata,
LinkMetadata,
LivestreamMetadata,
MintMetadata,
ProfileMetadata,
PublicationMetadata,
SpaceMetadata,
StoryMetadata,
TextOnlyMetadata,
ThreeDMetadata,
TransactionMetadata,
VideoMetadata,
// others
MetadataAttribute,
MediaAudio,
MediaImage,
MediaVideo,
AnyMedia,
GeoLocation,
BooleanAttribute,
DateAttribute,
NumberAttribute,
StringAttribute,
JSONAttribute,
// branded aliases
Locale,
Markdown,
Signature,
URI,
AppId,
Datetime,
} from '@lens-protocol/metadata';Importing JSON schema in TypeScript is a simple as:
import audio from '@lens-protocol/metadata/jsonschemas/post/audio/3.0.0.json' assert { type: 'json' };
import audio from '@lens-protocol/metadata/jsonschemas/post/article/3.0.0.json' assert { type: 'json' };
import mirror from '@lens-protocol/metadata/jsonschemas/post/mirror/1.0.0.json' assert { type: 'json' };
import profile from '@lens-protocol/metadata/jsonschemas/account/1.0.0.json' assert { type: 'json' };You can the use them in your JSON Schema validator of choice, for example ajv.
The Lens Protocol Metadata Standards use a self-describing JSON format. All metadata files that adopt this standard MUST have a $schema property that identifies the schema the file conforms to.
{
"$schema": "https://json-schemas.lens.dev/post/article/3.0.0.json",
"lens": {
"id": "b3d7f1a0-1f75-11ec-9621-0242ac130002",
"content": "The content of the article",
"locale": "en"
}
}The $schema property is a URI that identify the schema type and its version.
Schemas are versioned using Semantic Versioning.
Note
Even though schemas are identified by URIs, those identifiers are not necessarily network-addressable. They are just identifiers.
Generally, JSON schema validators don’t make HTTP requests (https://) to fetch schemas. Instead, they provide a way to load schemas into an internal schema database. When a schema is referenced by its URI identifier, the schema is retrieved from the internal schema database.
Future changes should aim to be backwards compatible as much as possible.
When adding a new version of a schema, the previous version should be kept for a reasonable amount of time to allow consumers to migrate and to support the new specification.
In this example we will add a new version of the AudioSchema schema, but the same process applies to all the other schemas.
- create a new
PostSchemaIdenum entry with value ofPostSchemaId.AUDIO_LATEST. Name it after the current schema version (e.g.AUDIO_V1_0_0). - rename the existing
AudioSchemaintoAudioV1_0_0Schemaand update the$schemavalue toPostSchemaId.AUDIO_V1_0_0 - increase the version number of the
PostSchemaId.AUDIO_LATESTbased on the nature of your changes. Remember to follow semver rules. - create a new
AudioSchemawith the new schema definition and use thePostSchemaId.AUDIO_LATESTas$schemavalue - update the
scripts/build.tsscript to include the new schema and old schema files under the correct version file name in thejsonschemas/post/audiofolder - release a new version of this package according to the nature of the changes (new major version of a schema = new major version of the package, etc.)
In case the changes are backwards compatible, you could create a single AudioMetadataDetailsSchema definition and just declare 2 schemas out of it, one for the old version and one for the new version. For example:
export const AudioMetadataDetailsSchema = metadataDetailsWith({
mainContentFocus: mainContentFocus(PostMainFocus.AUDIO),
audio: MediaAudioSchema,
attachments: AnyMediaSchema.array()
.min(1)
.optional()
.describe('The other attachments you want to include with it.'),
/** e.g. new optional fields */
});
export type AudioMetadataDetails = z.infer<typeof AudioMetadataDetailsSchema>;
export const AudioSchema = postWith({
$schema: z.literal(PostSchemaId.AUDIO_LATEST),
lens: AudioMetadataDetailsSchema,
});
export type AudioMetadata = z.infer<typeof AudioSchema>;
export const AudioV1Schema = postWith({
$schema: z.literal(PostSchemaId.AUDIO_V1_0_0),
lens: AudioMetadataDetailsSchema,
});
export type AudioV1Metadata = z.infer<typeof AudioV1Schema>;In this case consumers of this package can take advantage of the structural likeness and just do the following:
switch (metadata.$schema) {
case PostSchemaId.AUDIO_V1_0_0:
case PostSchemaId.AUDIO_LATEST:
// metadata.lens is AudioMetadataDetails
break;
// ...
}To contribute to the Lens Protocol Metadata Standards, please fork this repository and submit a pull request with your changes.
To run the unit tests, run:
pnpm testPro-tip: you can run pnpm test --watch to run the tests in watch mode.
To build the project, run:
pnpm buildGenerate and include up to date documentation with:
pnpm typedoc:docsAdd changeset with:
pnpm changeset addUse keepachangelog format for the changeset message.
Release flow is managed by changesets.
To release a new version follow the steps below:
- Create a new branch from
mainwith the namerelease/<version> - Build the project
pnpm install && pnpm build && pnpm typedoc:docs- Update relevant
package.json's versions and updateCHANGELOG.mdwith:
pnpm changeset version- Review, commit and push the changes
- Create a PR from
release/<version>tomain - Once approved, publish with (you need to be logged in to npm authorized to publish under
@lens-protocol):
pnpm changeset publish- Push the tags
git push origin release/<version> --follow-tags- Merge the PR with a merge commit
Lens Protocol Metadata Standards is MIT licensed
See the Lens API and SDK channel on our Discord