-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add UnknownJSONKeys, cleanup docs, and refactor for maintenance.
- Loading branch information
Charles Banning
committed
Feb 8, 2017
1 parent
abc405b
commit 193f024
Showing
5 changed files
with
407 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.