How to validate a payload schema

Marek SkopowskiMarek Skopowski
3 min read

Problem

Incoming request payloads in JSON format often requires validation.

Validators might get complex and messy.

We don’t want our codebase to be messy - at least we try not to.

Solution

JSON schema

The best way is to convert our JSON representation of the incoming request into the JSON schema with the help of one of the multiple converters online. I was using the Free Online JSON to JSON Schema Converter.

From the input JSON payload:

{"id":"022eb326-6fa2-4932-8a5f-058a908616a4"}

We’re getting the JSON shema that looks like this for the mentioned input:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "required": [
    "id"
  ]
}

Schema validation

Now let’s get to the code.

The validation function:

// ValidateSchema validates payload against JSON schema located in the schema file.
func ValidateSchema(payload map[string]any, schema string) error {
    // Let's read the schema file
    file, err := os.ReadFile(schema)
    if err != nil {
        return fmt.Errorf("os.ReadFile: %w", err)
    }

    // Prepare validators for schema...
    schemaLoader := gojsonschema.NewStringLoader(string(file))
    // ... and payload.
    payloadLoader := gojsonschema.NewGoLoader(payload)

    // Validate schema against payload:
    result, err := gojsonschema.Validate(schemaLoader, payloadLoader)
    if err != nil {
        return fmt.Errorf("gojsonschema.Validate: %w", err)
    }

    // If there was something wrong, communicate the errors:
    if !result.Valid() {
        errMsg := "JSON validation failed:\n"
        for _, desc := range result.Errors() {
            errMsg += fmt.Sprintf("- %s\n", desc)
        }
        return fmt.Errorf("%w: %s", ErrInvalidPayload, errMsg)
    }

    return nil
}

Working application

Let’s put all the piecen together (file main.go):

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "os"

    "github.com/google/uuid"
    "github.com/xeipuuv/gojsonschema"
)

var ErrInvalidPayload = errors.New("invalid payload")

type RequestPayload struct {
    ID uuid.UUID `json:"id"`
}

func main() {
    // Request payload.
    p, err := payload()
    if err != nil {
        log.Fatal(err)
    }

    // Validate incoming payload.
    if err = ValidateSchema(p, "schema/payload.json"); err != nil {
        log.Fatal(err)
    }

    // We're good!
    fmt.Println("Schema validated")
}

// ValidateSchema validates payload against JSON schema located in the schema file.
func ValidateSchema(payload map[string]any, schema string) error {
    // Let's read the schema file
    file, err := os.ReadFile(schema)
    if err != nil {
        return fmt.Errorf("os.ReadFile: %w", err)
    }

    // Prepare validators for schema...
    schemaLoader := gojsonschema.NewStringLoader(string(file))
    // ... and payload.
    payloadLoader := gojsonschema.NewGoLoader(payload)

    // Validate schema against payload:
    result, err := gojsonschema.Validate(schemaLoader, payloadLoader)
    if err != nil {
        return fmt.Errorf("gojsonschema.Validate: %w", err)
    }

    // If there was something wrong, communicate the errors:
    if !result.Valid() {
        errMsg := "JSON validation failed:\n"
        for _, desc := range result.Errors() {
            errMsg += fmt.Sprintf("- %s\n", desc)
        }
        return fmt.Errorf("%w: %s", ErrInvalidPayload, errMsg)
    }

    return nil
}

// payload returns sample request payload in map[string]any format.
func payload() (map[string]any, error) {
    r := &RequestPayload{ID: uuid.New()}

    payload, err := json.Marshal(r)
    if err != nil {
        return nil, fmt.Errorf("json.Marshal: %w", err)
    }

    var data map[string]any
    if err := json.Unmarshal(payload, &data); err != nil {
        return nil, fmt.Errorf("json.Unmarshal: %w", err)
    }

    return data, nil
}
➜  payloadschema git:(main) ✗ go run main.go
Schema validated

Sources

0
Subscribe to my newsletter

Read articles from Marek Skopowski directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Marek Skopowski
Marek Skopowski

Software Engineer x Data Engineer - I make the world a better place to live with software that enables data-driven decision-making