Frontend Labs
⋯

Day 78 — JSON Schema Enforcement

Learning Objectives

  • Implement a JSON Schema validator for API request bodies
  • Reject unknown fields (additionalProperties: false)
  • Return structured validation errors

Key Concepts

Why JSON Schema validation matters:
  - additionalProperties: false blocks parameter pollution
    {"username":"alice","password":"x","admin":true} → rejected
  - type enforcement catches coercion attacks
    {"amount":"100"} when amount should be number → rejected
  - required prevents partial submissions that bypass logic

JSON Schema subset to implement:
  type:              string|number|boolean|array|object|null
  required:          ["field1","field2"]
  minLength/maxLength for strings
  minimum/maximum for numbers
  pattern:           regex for strings
  additionalProperties: false
  items:             schema for array elements
  properties:        map of field name → schema

Exercise: JSON Schema Validator

package jsonschema

import (
    "encoding/json"
    "fmt"
    "math"
    "regexp"
)

type Schema struct {
    Type                 string             `json:"type"`
    Required             []string           `json:"required"`
    Properties           map[string]*Schema `json:"properties"`
    AdditionalProperties *bool              `json:"additionalProperties"`
    MinLength            *int               `json:"minLength"`
    MaxLength            *int               `json:"maxLength"`
    Minimum              *float64           `json:"minimum"`
    Maximum              *float64           `json:"maximum"`
    Pattern              string             `json:"pattern"`
    Enum                 []interface{}      `json:"enum"`
    Items                *Schema            `json:"items"`
}

type SchemaError struct {
    Path    string
    Message string
}

func (e SchemaError) Error() string {
    if e.Path != "" {
        return fmt.Sprintf("%s: %s", e.Path, e.Message)
    }
    return e.Message
}

// Validate validates data against schema, returning all errors
func (s *Schema) Validate(data interface{}, path string) []SchemaError {
    var errs []SchemaError

    // Type check
    if s.Type != "" {
        if !checkType(data, s.Type) {
            return []SchemaError{{path, fmt.Sprintf("expected %s, got %T", s.Type, data)}}
        }
    }

    switch val := data.(type) {
    case map[string]interface{}:
        // Check required fields
        for _, req := range s.Required {
            if _, ok := val[req]; !ok {
                errs = append(errs, SchemaError{path + "." + req, "required field missing"})
            }
        }

        // Validate properties
        for key, fieldSchema := range s.Properties {
            if v, ok := val[key]; ok {
                errs = append(errs, fieldSchema.Validate(v, path+"."+key)...)
            }
        }

        // Reject unknown fields if additionalProperties: false
        if s.AdditionalProperties != nil && !*s.AdditionalProperties {
            for key := range val {
                if _, ok := s.Properties[key]; !ok {
                    errs = append(errs, SchemaError{path + "." + key, "additional property not allowed"})
                }
            }
        }

    case string:
        if s.MinLength != nil && len(val) < *s.MinLength {
            errs = append(errs, SchemaError{path, fmt.Sprintf("min length %d, got %d", *s.MinLength, len(val))})
        }
        if s.MaxLength != nil && len(val) > *s.MaxLength {
            errs = append(errs, SchemaError{path, fmt.Sprintf("max length %d, got %d", *s.MaxLength, len(val))})
        }
        if s.Pattern != "" {
            re, err := regexp.Compile(s.Pattern)
            if err == nil && !re.MatchString(val) {
                errs = append(errs, SchemaError{path, fmt.Sprintf("pattern %q not matched", s.Pattern)})
            }
        }

    case float64:
        if s.Minimum != nil && val < *s.Minimum {
            errs = append(errs, SchemaError{path, fmt.Sprintf("minimum %g, got %g", *s.Minimum, val)})
        }
        if s.Maximum != nil && val > *s.Maximum {
            errs = append(errs, SchemaError{path, fmt.Sprintf("maximum %g, got %g", *s.Maximum, val)})
        }
    }

    return errs
}

func checkType(data interface{}, t string) bool {
    switch t {
    case "string":  _, ok := data.(string); return ok
    case "number":  _, ok := data.(float64); return ok
    case "boolean": _, ok := data.(bool); return ok
    case "array":   _, ok := data.([]interface{}); return ok
    case "object":  _, ok := data.(map[string]interface{}); return ok
    case "null":    return data == nil
    }
    return true
}

Real-World Context

additionalProperties: false is the parameter pollution defense. In 2020, a password reset vulnerability was found where sending {"email":"victim","admin":true} was accepted because there was no schema validation. The extra field was silently processed. JSON Schema with additionalProperties: false blocks this class of attack entirely.


Generated from week11.md · 8 Oct 2026 16:36