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.