Sphereon-Opensource/OID4VC

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

nklomp commented

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