Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leverage zod (or similar library) for validation #77

Open
TimoGlastra opened this issue Nov 16, 2023 · 1 comment
Open

Leverage zod (or similar library) for validation #77

TimoGlastra opened this issue Nov 16, 2023 · 1 comment

Comments

@TimoGlastra
Copy link
Collaborator

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
Copy link
Contributor

nklomp commented Nov 23, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants