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 zod
Note
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
PostSchemaId
enum entry with value ofPostSchemaId.AUDIO_LATEST
. Name it after the current schema version (e.g.AUDIO_V1_0_0
). - rename the existing
AudioSchema
intoAudioV1_0_0Schema
and update the$schema
value toPostSchemaId.AUDIO_V1_0_0
- increase the version number of the
PostSchemaId.AUDIO_LATEST
based on the nature of your changes. Remember to follow semver rules. - create a new
AudioSchema
with the new schema definition and use thePostSchemaId.AUDIO_LATEST
as$schema
value - update the
scripts/build.ts
script to include the new schema and old schema files under the correct version file name in thejsonschemas/post/audio
folder - 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 test
Pro-tip: you can run pnpm test --watch
to run the tests in watch mode.
To build the project, run:
pnpm build
Generate and include up to date documentation with:
pnpm typedoc:docs
Add changeset with:
pnpm changeset add
Use 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
main
with the namerelease/<version>
- Build the project
pnpm install && pnpm build && pnpm typedoc:docs
- Update relevant
package.json
's versions and updateCHANGELOG.md
with:
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