Leverage zod (or similar library) for validation
TimoGlastra opened this issue · 1 comments
The validation within this library is mainly done manually. Because there's a lot of models and types, this can result in a lot of manual validation logic.
We've been adopting zod into our stack, and it has been working very well for us. You define a zod object, which defines the validation logic, and based on that you can infer the typescript type (as a simple type).
This way you can easily validate every interface you have in your library. It does add some overhead as you need to validate, but I don't think it will be very significant, and it does allow to clean up a lot of the validation code.
You can use things like refine
to have very special validation logic, and I think this would enforce separating validation logic from the actual implementation side of the spec.
An example with some of the credential offer types
import { z } from 'zod'
const zJwtVcJsonOfferFormat = z.object({
format: z.literal('jwt_vc_json'),
types: z.array(z.string())
})
const zLdpVcOfferFormat = z.object({
format: z.literal('ldp_vc'),
"@context": z.array(z.string().url()),
types: z.array(z.string())
})
const zCredentialOfferFormat = z.discriminatedUnion("format", [zJwtVcJsonOfferFormat, zLdpVcOfferFormat])
const zCredentialOfferPayload = z.object({
credential_issuer: z.string().url(),
credentials: z.array(z.union([z.string(), zCredentialOfferFormat])).nonempty()
})
const zCredentialOffer = z.union([
z.object({
credential_offer_uri: z.string().url(),
}),
z.object({
credential_offer: zCredentialOfferPayload
})
])
type CredentialOfferPayload = z.infer<typeof zCredentialOfferPayload>
type CredentialOffer = z.infer<typeof zCredentialOffer>
const results = [
zCredentialOffer.safeParse({
credential_offer_uri: 'https://google.com'
}), // valid
zCredentialOffer.safeParse({
credential_offer: {
credential_issuer: 'https://google.com',
credentials: []
} satisfies CredentialOfferPayload
}), // invalid, credentials must have at least one entry
zCredentialOffer.safeParse({
credential_offer: {
credential_issuer: 'https://google.com',
credentials: [{
format: 'ldp_vc',
"@context": ["https://google.com"],
types: ["VerifiableCredential"]
}]
} satisfies CredentialOfferPayload
}), // valid
zCredentialOffer.safeParse({
credential_offer: {
credential_issuer: 'https://google.com',
credentials: [{
format: 'jwt_vc_json',
"@context": ["https://google.com"],
types: ["VerifiableCredential"]
}]
} satisfies CredentialOfferPayload
}) // "@context" is not defiend for jwt_vc_json. If we enable .strict() on the object it will error, but by default it will remove unknown properties
]
console.log(JSON.stringify(results, null, 2))
This results in the following output:
[
{
"success": true,
"data": {
"credential_offer_uri": "https://google.com/"
}
},
{
"success": false,
"error": {
"issues": [
{
"code": "too_small",
"minimum": 1,
"type": "array",
"inclusive": true,
"exact": false,
"message": "Array must contain at least 1 element(s)",
"path": [
"credential_offer",
"credentials"
]
}
],
"name": "ZodError"
}
},
{
"success": true,
"data": {
"credential_offer": {
"credential_issuer": "https://google.com/",
"credentials": [
{
"format": "ldp_vc",
"@context": [
"https://google.com/"
],
"types": [
"VerifiableCredential"
]
}
]
}
}
},
{
"success": true,
"data": {
"credential_offer": {
"credential_issuer": "https://google.com/",
"credentials": [
{
"format": "jwt_vc_json",
"types": [
"VerifiableCredential"
]
}
]
}
}
}
]
Here's a codesandbox with the code: https://codesandbox.io/s/typescript-playground-export-forked-nrwgg4
Yes for sure like this approach. Given it is a standalone library with 0 deps focused on something we now do manually I have no objections to using something like this, as it will only make all our lives better and reduce the amount of bugs