I was experiencing something similar. I wanted to be able to load data from another service, unmarshal the data into a struct, array or whatever, as usual.
However I didn't want to expose some of the fields to the frontend, however i still need them internally. So using json:"-", wouldn't help because then the field would not be parsed at all into the struct field. json:"omitempty" just helps not outputting nil or empty values. Since my values would not be empty, they would be outputted.
I made a function that basically using reflect, but also introduced a new flag to the json tag.
I have tested it for a while, so feel free to use it. However be careful, I can't say that it works for all different cases. I haven't experienced any issues yet though.
Here comes an example how to use it.
Usage:
type User struct {
InternalId string `json:"internal_id,outputoff"`
Name string `json:"name"`
}
func myHttpHandlerFunc(w http.ResponseWriter, r *http.Request) {
// imagine this is loaded from somewhere else
u := User{
InternalId: "whatever_id",
Name: "User Name",
}
Response(w, u)
}
func Response(w http.ResponseWriter, response interface{}) {
res, _ := MarshalExcludeJSONTag(response)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
w.Write(res)
}
This will only output the following JSON:
{
"name": "User Name"
}
The code
import (
"encoding/json"
"fmt"
"reflect"
)
func MarshalExcludeJSONTag(v interface{}) ([]byte, error) {
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
// Handle pointer types: dereference to access the actual value
if val.Kind() == reflect.Ptr {
if val.IsNil() {
// If the pointer is nil, we can return an empty JSON array if it's a slice
if typ.Elem().Kind() == reflect.Slice {
return []byte("[]"), nil
}
return nil, fmt.Errorf("cannot marshal nil pointer")
}
val = val.Elem()
typ = typ.Elem()
}
// If it's not a struct, slice, or map, we can directly marshal
if val.Kind() != reflect.Struct && val.Kind() != reflect.Slice && val.Kind() != reflect.Map {
return json.Marshal(v)
}
// To track seen pointers and avoid infinite recursion for circular references
seen := make(map[uintptr]bool)
if val.Kind() == reflect.Map {
resultMap := make(map[string]interface{})
for _, key := range val.MapKeys() {
// Ensure key is convertible to a string
if key.Kind() == reflect.Complex64 || key.Kind() == reflect.Complex128 {
return nil, fmt.Errorf("unsupported map key type: %v", key.Kind())
}
strKey := fmt.Sprintf("%v", key.Interface())
// Process the value recursively
value := val.MapIndex(key)
processedValue, err := MarshalExcludeInJSONTag(value.Interface())
if err != nil {
return nil, err
}
var temp interface{}
err = json.Unmarshal(processedValue, &temp)
if err != nil {
return nil, err
}
resultMap[strKey] = temp
}
return json.Marshal(resultMap)
}
// If it's a slice, we need to handle each element of the slice
if val.Kind() == reflect.Slice {
var result []interface{}
for i := 0; i < val.Len(); i++ {
elem := val.Index(i)
// Check for circular references in slices
if elem.Kind() == reflect.Ptr && elem.IsNil() {
result = append(result, nil)
continue
}
if elem.Kind() == reflect.Ptr && seen[elem.Addr().Pointer()] {
// Prevent infinite recursion
result = append(result, nil)
continue
}
seen[elem.Addr().Pointer()] = true
if elem.Kind() == reflect.Struct {
// Marshal each struct inside the slice individually
elemData, err := MarshalExcludeInJSONTag(elem.Interface())
if err != nil {
return nil, err
}
// Unmarshal into a generic interface{} to return the filtered struct
var temp interface{}
err = json.Unmarshal(elemData, &temp)
if err != nil {
return nil, err
}
result = append(result, temp)
} else {
// If it's not a struct, just append the value
result = append(result, elem.Interface())
}
}
// Marshal the result slice and return
return json.Marshal(result)
}
// If it's not a slice, proceed with struct processing as before
var resultFields []reflect.StructField
var resultValues []reflect.Value
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldValue := val.Field(i)
// Check if the "json" tag contains the "outputoff" keyword
jsonTag := field.Tag.Get("json")
if strings.Contains(strings.Replace(jsonTag, " ", "", -1), ",outputoff") {
continue
}
// Handle arrays or slices recursively
if fieldValue.Kind() == reflect.Slice {
// Create a slice of the correct type
newSlice := reflect.MakeSlice(fieldValue.Type(), 0, fieldValue.Len())
for i := 0; i < fieldValue.Len(); i++ {
elem := fieldValue.Index(i)
if elem.Kind() == reflect.Struct {
// Marshal nested struct elements recursively
elemData, err := MarshalExcludeInJSONTag(elem.Interface())
if err != nil {
return nil, err
}
// Create a pointer to the expected struct type
newElem := reflect.New(elem.Type()).Interface()
// Unmarshal directly into the correct struct type
err = json.Unmarshal(elemData, newElem)
if err != nil {
return nil, err
}
newSlice = reflect.Append(newSlice, reflect.ValueOf(newElem).Elem())
} else {
// Non-struct type: directly append
newSlice = reflect.Append(newSlice, elem)
}
}
resultFields = append(resultFields, field)
resultValues = append(resultValues, newSlice)
} else {
// Regular field: add it as-is
resultFields = append(resultFields, field)
resultValues = append(resultValues, fieldValue)
}
}
// Construct a new struct based on the modified fields
resultStruct := reflect.New(reflect.StructOf(resultFields)).Elem()
for i := 0; i < len(resultFields); i++ {
resultStruct.Field(i).Set(resultValues[i])
}
// Marshal the new struct to JSON
return json.Marshal(resultStruct.Interface())
}
If you don't need to remove fields a lot, this would be an overkill, and just an extra layer and risk, so make sure it is worth it.
Enjoy and Use with caution!