Skip to content

Commit

Permalink
Add UnknownJSONKeys, cleanup docs, and refactor for maintenance.
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles Banning committed Feb 8, 2017
1 parent abc405b commit 193f024
Show file tree
Hide file tree
Showing 5 changed files with 407 additions and 185 deletions.
5 changes: 3 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ Check that a JSON object's keys correspond to a struct's exported members or JSO

ANNOUNCEMENTS

2016.11.18 - MissingJSONKeys() lists all struct members that won't be set by JSON object.
2017.02.08 - UnknownJSONKeys lists all JSON object keys that won't be decoded.
2016.11.18 - MissingJSONKeys lists all struct members that won't be set by JSON object.

USAGE

Expand All @@ -11,7 +12,7 @@ https://godoc.org/github.com/clbanning/checkjson
EXAMPLE

import "github.com/clbanning/checkjson"

...
type Home struct {
Addr string
Port int
Expand Down
189 changes: 189 additions & 0 deletions missingkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// validate.go - check JSON object against struct definition
// Copyright © 2016-2017 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// ==== from: https://forum.golangbridge.org/t/how-to-detect-missing-bool-field-in-json-post-data/3861

package checkjson

import (
"encoding/json"
"fmt"
"reflect"
"strings"
)

type skipmems struct {
val string
depth int
}

var skipmembers = []skipmems{}

// SetMembersToIgnore creates a list of exported struct member names that should not be checked
// for as keys in the JSON object. For hierarchical struct members provide the full path for
// the member name using dot-notation. Calling SetMembersToIgnore with no arguments -
// SetMembersToIgnore() - will clear the list.
func SetMembersToIgnore(s ...string) {
if len(s) == 0 {
skipmembers = skipmembers[:0]
return
}
skipmembers = make([]skipmems, len(s))
for i, v := range s {
skipmembers[i] = skipmems{strings.ToLower(v), len(strings.Split(v, "."))}
}
}

// MissingJSONKeys returns a list of members of val of struct type that will NOT be set
// by unmarshaling the JSON object; rather, they will assume their default
// values. For nested structs, member labels are the dot-notation hierachical
// path for the missing JSON key. Specific struct members can be igored
// when scanning the JSON object by declaring them using SetMembersToIgnore().
// (NOTE: JSON object keys are treated as case insensitive, i.e., there
// is no distiction between "key":"value" and "Key":"value".)
func MissingJSONKeys(b []byte, val interface{}) ([]string, error) {
s := make([]string, 0)
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return s, ResolveJSONError(b, err)
}
if err := checkMembers(m, reflect.ValueOf(val), &s, ""); err != nil {
return s, err
}
return s, nil
}

// cmem is the parent struct member for nested structs
func checkMembers(mv interface{}, val reflect.Value, s *[]string, cmem string) error {
// 1. Convert any pointer value.
if val.Kind() == reflect.Ptr {
val = reflect.Indirect(val) // convert ptr to struc
}
// zero Value?
if !val.IsValid() {
return nil
}
typ := val.Type()

// json.RawMessage is a []byte/[]uint8 and has Kind() == reflect.Slice
if typ.Name() == "RawMessage" {
return nil
}

// 2. If its a slice then 'mv' should hold a []interface{} value.
// Loop through the members of 'mv' and see that they are valid relative
// to the <T> of val []<T>.
if typ.Kind() == reflect.Slice {
tval := typ.Elem()
if tval.Kind() == reflect.Ptr {
tval = tval.Elem()
}
// slice may be nil, so create a Value of it's type
// 'mv' must be of type []interface{}
sval := reflect.New(tval)
slice, ok := mv.([]interface{})
if !ok {
return fmt.Errorf("JSON value not an array")
}
// 2.1. Check members of JSON array.
// This forces all of them to be regular and w/o typos in key labels.
for n, sl := range slice {
// cmem is the member name for the slice - []<T> - value
if err := checkMembers(sl, sval, s, cmem); err != nil {
return fmt.Errorf("[array element #%d] %s", n+1, err.Error())
}
}
return nil // done with reflect.Slice value
}

// 3a. Ignore anything that's not a struct.
if typ.Kind() != reflect.Struct {
return nil // just ignore it - don't look for k:v pairs
}
// 3b. map value must represent k:v pairs
mm, ok := mv.(map[string]interface{})
if !ok {
return fmt.Errorf("JSON object does not have k:v pairs for member: %s",
typ.Name)
}
// 3c. Coerce keys to lower case.
mkeys := make(map[string]interface{}, len(mm))
for k, v := range mm {
mkeys[strings.ToLower(k)] = v
}

// 4. Build the list of struct field name:value
// We make every key (field) label look like an exported label - "Fieldname".
// If there is a JSON tag it is used instead of the field label, and saved to
// insure that the spec'd tag matches the JSON key exactly.
type fieldSpec struct {
name string
val reflect.Value
tag string
}
fieldCnt := val.NumField()
fields := make([]*fieldSpec, fieldCnt) // use a list so members are in sequence
for i := 0; i < fieldCnt; i++ {
if len(typ.Field(i).PkgPath) > 0 {
continue // field is NOT exported
}
tag := typ.Field(i).Tag.Get("json")
if tag == "" {
fields[i] = &fieldSpec{typ.Field(i).Name, val.Field(i), ""}
} else {
fields[i] = &fieldSpec{typ.Field(i).Name, val.Field(i), tag}
}
}

// 5. check that field names/tags have corresponding map key
// var ok bool
var v interface{}
var err error
cmemdepth := 1
if len(cmem) > 0 {
cmemdepth = len(strings.Split(cmem, ".")) + 1 // struct hierarchy
}
lcmem := strings.ToLower(cmem)
for _, field := range fields {
lm := strings.ToLower(field.name)
for _, sm := range skipmembers {
// skip any skipmembers values that aren't at same depth
if cmemdepth != sm.depth {
continue
}
if len(cmem) > 0 {
if lcmem+`.`+lm == sm.val {
goto next
}
} else if lm == sm.val {
goto next
}
}
if len(field.tag) > 0 {
v, ok = mkeys[field.tag]
} else {
v, ok = mkeys[lm]
}
if !ok {
if len(cmem) > 0 {
*s = append(*s, cmem+`.`+field.name)
} else {
*s = append(*s, field.name)
}
goto next // don't drill down further; no key in JSON object
}
if len(cmem) > 0 {
err = checkMembers(v, field.val, s, cmem+`.`+field.name)
} else {
err = checkMembers(v, field.val, s, field.name)
}
if err != nil { // could be nested structs
return fmt.Errorf("checking submembers of member: %s - %s", cmem, err.Error())
}
next:
}

return nil
}
143 changes: 143 additions & 0 deletions unknownkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// validate.go - check JSON object against struct definition
// Copyright © 2016-2017 Charles Banning. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package checkjson

import (
"encoding/json"
"reflect"
"strconv"
"strings"
)

// UnknownJSONKeys() returns a list of the JSON object keys that will not
// be decoded to a member of 'val', which is of type struct. For nested
// JSON objects the keys are reported using dot-notation.
func UnknownJSONKeys(b []byte, val interface{}) ([]string, error) {
s := make([]string, 0)
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
if err := checkAllFields(m, reflect.ValueOf(val), &s, ""); err != nil {
return s, err
}
return s, nil
}

func checkAllFields(mv interface{}, val reflect.Value, s *[]string, key string) error {
// get the name when initially called
if key == "" {
key = val.Type().Name()
}

// 1. Convert any pointer value.
if val.Kind() == reflect.Ptr {
val = reflect.Indirect(val) // convert ptr to struct
}
// zero Value?
if !val.IsValid() {
return nil
}
typ := val.Type()

// json.RawMessage is a []byte/[]uint8 and has Kind() == reflect.Slice
if typ.Name() == "RawMessage" {
return nil
}

// 2. If its a slice then 'mv' should hold a []interface{} value.
// Loop through the members of 'mv' and see that they are valid relative
// to the <T> of val []<T>.
if typ.Kind() == reflect.Slice {
tval := typ.Elem()
if tval.Kind() == reflect.Ptr {
tval = tval.Elem()
}
// slice may be nil, so create a Value of it's type
// 'mv' must be of type []interface{}
sval := reflect.New(tval)
slice, ok := mv.([]interface{})
if !ok {
// return fmt.Errorf("JSON value not an array")
*s = append(*s, key)
return nil
}
// 2.1. Check members of JSON array.
// This forces all of them to be regular and w/o typos in key labels.
for n, sl := range slice {
_ = checkAllFields(sl, sval, s, key+"."+strconv.Itoa(n+1))
// if err := checkAllFields(sl, sval, s, key+strconv.Itoa(n+1)); err != nil {
// return fmt.Errorf("[array element #%d] %s", n+1, err.Error())
// }
}
return nil // done with reflect.Slice value
}

// 3a. Ignore anything that's not a struct.
if typ.Kind() != reflect.Struct {
return nil // just ignore it - don't look for k:v pairs
}
// 3b. map value must represent k:v pairs
mm, ok := mv.(map[string]interface{})
if !ok {
*s = append(*s, key)
}
// if !ok {
// return fmt.Errorf("JSON object does not have k:v pairs for member: %s",
// typ.Name)
// }

// 4. Build the map of struct field name:value
// We make every key (field) label look like an exported label - "Fieldname".
// If there is a JSON tag it is used instead of the field label, and saved to
// insure that the spec'd tag matches the JSON key exactly.
type fieldSpec struct {
val reflect.Value
tag string
}
fieldCnt := val.NumField()
fields := make(map[string]*fieldSpec, fieldCnt)
for i := 0; i < fieldCnt; i++ {
if len(typ.Field(i).PkgPath) > 0 {
continue // field is NOT exported
}
tag := typ.Field(i).Tag.Get("json")
if tag == "" {
fields[strings.Title(strings.ToLower(typ.Field(i).Name))] = &fieldSpec{val.Field(i), ""}
} else {
fields[strings.Title(strings.ToLower(tag))] = &fieldSpec{val.Field(i), tag}
}
}

// 5. check that map keys correspond to exported field names
var spec *fieldSpec
for k, m := range mm {
lk := strings.ToLower(k)
for _, sk := range skipkeys {
if lk == sk {
goto next
}
}
spec, ok = fields[strings.Title(lk)]
if !ok {
*s = append(*s, key+"."+lk)
return nil
// return fmt.Errorf("no member for JSON key: %s", k)
}
if len(spec.tag) > 0 && spec.tag != k { // JSON key doesn't match Field tag
// return fmt.Errorf("key: %s - does not match tag: %s", k, spec.tag)
*s = append(*s, key+"["+spec.tag+"]") // include tag in brackets
return nil
}
_ = checkAllFields(m, spec.val, s, key+"."+lk)
// if err := checkFields(m, spec.val); err != nil { // could be nested structs
// return fmt.Errorf("checking subkeys of JSON key: %s - %s", k, err.Error())
// }
next:
}

return nil
}
Loading

0 comments on commit 193f024

Please sign in to comment.