Control field visibility depending on User role - json

I'd like to hide/show some fields of a model depending on User role.
What would be the most idiomatic way to implement it?
I don't really want to create N different types of the same model (where N is amount of User roles). Like:
UserEmployee, AdminEmployee, WhateverEmployee.
It would be perfect if there is some solution that uses the tags for it:
type Employee struct {
ID string `visibility:"admin,user"`
Name string `visibility:"admin,user"`
Salary int `visibility:"admin"`
}
jsonBytes, _ := someLib.Marshal(Employee{"1", "John", 5000}, "user")
fmt.Println(string(jsonBytes)) // {"id":"1","name":"John"}
The question is really pretty broad. I just wanted to know how you handle this situation or what is the most common way to do it in the Go community. I want clean and centralized (same for all models) solution that won't require to produce tons of duplicated code.
What have I tried before: I've just tried to use separate models for all cases and cast between them.

Create an empty struct of your type (Employee in this problem) that will hold the filtered data.
Use the reflect package to compare if the field tag contains the desired tag value (visibility role).
Copy values of base struct to our filter struct when we find a tag match and json marshal the output struct:
package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
type Employee struct {
ID string `visibility:"admin, hr, user" json:"id,omitempty"`
Name string `visibility:"admin, hr, user" json:"name,omitempty"`
Salary int `visibility:"admin, hr" json:"salary,omitempty"`
Password string `visibility:"admin" json:"password,omitempty"`
Rights map[string]bool `visibility:"admin" json:"rights,omitempty"`
Boss *Employee `visibility:"admin, hr" json:"boss,omitempty"`
}
func filterEmployee(emp Employee, role string) Employee {
var fEmployee Employee
ev := reflect.ValueOf(emp)
et := reflect.TypeOf(emp)
// Iterate through each field within the struct
for i := 0; i < ev.NumField(); i++ {
v := ev.Field(i)
t := et.Field(i)
roles := t.Tag.Get("visibility")
if strings.Contains(roles, role) {
switch i {
case 0: // ID
fEmployee.ID = v.String()
case 1: // Name
fEmployee.Name = v.String()
case 2: // Salary
fEmployee.Salary = int(v.Int())
case 3: // Password
fEmployee.Password = v.String()
case 4: // Rights
fEmployee.Rights = v.Interface().(map[string]bool)
case 5: // Boss
fEmployee.Boss = v.Interface().(*Employee)
}
}
}
return fEmployee
}
func main() {
e := Employee{
"1",
"Jack",
100000,
"password321",
map[string]bool{"create": false, "update": false},
&Employee{
"2",
"John",
120000,
"pwd",
map[string]bool{"create": true, "update": true},
nil,
},
}
fuser := filterEmployee(e, "user")
fhr := filterEmployee(e, "hr")
fadmin := filterEmployee(e, "admin")
buser, err := json.MarshalIndent(fuser, "", " ")
if err != nil {
fmt.Println(err)
}
fmt.Println("Filtering with role user: ")
fmt.Println(string(buser))
bhr, err := json.MarshalIndent(fhr, "", " ")
if err != nil {
fmt.Println(err)
}
fmt.Println("\nFiltering with role hr: ")
fmt.Println(string(bhr))
badmin, err := json.MarshalIndent(fadmin, "", " ")
if err != nil {
fmt.Println(err)
}
fmt.Println("\nFiltering with role admin: ")
fmt.Println(string(badmin))
}
Output:
Filtering with role user:
{
"id": "1",
"name": "Jack"
}
Filtering with role hr:
{
"id": "1",
"name": "Jack",
"salary": 100000,
"boss": {
"id": "2",
"name": "John",
"salary": 120000,
"password": "pwd",
"rights": {
"create": true,
"update": true
}
}
}
Filtering with role admin:
{
"id": "1",
"name": "Jack",
"salary": 100000,
"password": "password321",
"rights": {
"create": false,
"update": false
},
"boss": {
"id": "2",
"name": "John",
"salary": 120000,
"password": "pwd",
"rights": {
"create": true,
"update": true
}
}
}
Playground
EDIT: Updated answer for asker's request.
View the old playground for previous answer that ran into issues.
Old Playground

Use "omit empty"
type Employee struct {
ID string `json:",omitempty"`
Name string `json:",omitempty"`
Salary int `json:",omitempty"`
}
Your function can look like
func MarshallEmployee(e Employee, permission string) {
if permission == "user"{
e.Salary = 0
}
....marshall it
}
or you could also just not add the value to the struct in the first place. See the docs for more detail.

use this module: https://github.com/icoom-lab/marian/
package main
import (
"fmt"
"github.com/icoom-lab/marian"
)
type Account struct {
Id int `json:"id,omitempty" role:"admin"`
Name string `json:"name,omitempty" role:"admin,normal"`
}
func main() {
account := Account{
Id: 1,
Name: "Jhon",
}
fmt.Println(account)
// {id:1, Name:"Jhon"}
marian.CleanStruct("role", "admin", &account)
fmt.Println(account)
// {id:1, Name:"Jhon"}
marian.CleanStruct("role", "normal", &account)
fmt.Println(account)
// {Name:"Jhon"}
}

Related

Golang: Validate inner Struct field based on the values of one of its enclosing struct's field using required_if tag

golang version: 1.18.3
validator: github.com/go-playground/validator/v10
I want to validate an incoming JSON payload after loaded into nested struct data structure. Here's my incoming JSON payload,
{
"irn": 1,
"firstName": "Testing",
"lastName": "Test",
"cardType": "RECIPROCAL",
"number": "2248974514",
"cardExpiry": {
"month": "01",
"year": "2025"
}
}
here's my medicare.go file
package main
import (
"encoding/json"
"github.com/go-playground/validator/v10"
)
type Medicare struct {
IRN uint8
FirstName string
MiddleInitial string
LastName string
CardType string `validate:"required,eq=RESIDENT|eq=RECIPROCAL|eq=INTERIM"`
Number string
CardExpiry *CardExpiry `validate:"required"`
}
type CardExpiry struct {
Day string
Month string `validate:"required"`
Year string `validate:"required"`
}
Here's my test function
func TestUserPayload(t *testing.T) {
var m Medicare
err := json.Unmarshal([]byte(jsonData), &m)
if err != nil {
panic(err)
}
validate := validator.New()
err = validate.Struct(m)
if err != nil {
t.Errorf("error %v", err)
}
}
I want to do the following validation using validator/v10's required_if tag.
Here's the validation logic,
if (m.CardType == "RECIPROCAL" || m.CardType == "INTERIM") &&
m.CardExpiry.Day == "" {
//validation error
}
required_if can be used based on the field values which are in the same struct (in this case CardExpiry)
Day string `validate:"required_if=Month 01"`
My question is,
can it be done based on the values of one of its enclosing struct's field (in this case Medicare struct)?
for ex:
Day string `validate:"required_if=Medicare.CardType RECIPROCAL"`
if can, how?
Here's the go playground code
you could write custom validation,
playground-solution, documentation for custom validation is available here https://pkg.go.dev/github.com/go-playground/validator#CustomTypeFunc.
Slightly old question, but valix (https://github.com/marrow16/valix) can do such things 'out-of-the-box' using conditions. Example...
package main
import (
"fmt"
"net/http"
"strings"
"github.com/marrow16/valix"
)
type Medicare struct {
IRN uint8 `json:"irn"`
FirstName string `json:"firstName"`
MiddleInitial string `json:"middleInitial"`
LastName string `json:"lastName"`
// set order on this property so that it is evaluated before 'CardExpiry' object is checked...
// (and set a condition based on its value)
CardType string `json:"cardType" v8n:"order:-1,required,&StringValidToken{['RESIDENT','RECIPROCAL','INTERIM']},&SetConditionFrom{Global:true}"`
Number string `json:"number"`
CardExpiry *CardExpiry `json:"cardExpiry" v8n:"required,notNull"`
}
type CardExpiry struct {
// this property is required when a condition of `RECIPROCAL` has been set (and unwanted when that condition has not been set)...
Day string `json:"day" v8n:"required:RECIPROCAL,unwanted:!RECIPROCAL"`
Month string `json:"month" v8n:"required"`
Year string `json:"year" v8n:"required"`
}
var medicareValidator = valix.MustCompileValidatorFor(Medicare{}, nil)
func main() {
jsonData := `{
"irn": 1,
"firstName": "Testing",
"lastName": "Test",
"cardType": "RECIPROCAL",
"number": "2248974514",
"cardExpiry": {
"month": "01",
"year": "2025"
}
}`
medicare := &Medicare{}
ok, violations, _ := medicareValidator.ValidateStringInto(jsonData, medicare)
// should fail with 1 violation...
fmt.Printf("First ok?: %v\n", ok)
for i, v := range violations {
fmt.Printf("Violation[%d]: Message:%s, Property:%s, Path:%s\n", i+1, v.Message, v.Property, v.Path)
}
jsonData = `{
"irn": 1,
"firstName": "Testing",
"lastName": "Test",
"cardType": "RECIPROCAL",
"number": "2248974514",
"cardExpiry": {
"day": "01",
"month": "01",
"year": "2025"
}
}`
ok, _, _ = medicareValidator.ValidateStringInto(jsonData, medicare)
// should be ok...
fmt.Printf("Second ok?: %v\n", ok)
jsonData = `{
"irn": 1,
"firstName": "Testing",
"lastName": "Test",
"cardType": "INTERIM",
"number": "2248974514",
"cardExpiry": {
"month": "01",
"year": "2025"
}
}`
ok, _, _ = medicareValidator.ValidateStringInto(jsonData, medicare)
// should be ok...
fmt.Printf("Third ok?: %v\n", ok)
jsonData = `{
"irn": 1,
"firstName": "Testing",
"lastName": "Test",
"cardType": "INTERIM",
"number": "2248974514",
"cardExpiry": {
"day": "01",
"month": "01",
"year": "2025"
}
}`
ok, violations, _ = medicareValidator.ValidateStringInto(jsonData, medicare)
fmt.Printf("Fourth ok?: %v\n", ok)
for i, v := range violations {
fmt.Printf("Violation[%d]: Message:%s, Property:%s, Path:%s\n", i+1, v.Message, v.Property, v.Path)
}
// or validate directly from a http request...
req, _ := http.NewRequest("POST", "", strings.NewReader(jsonData))
ok, violations, _ = medicareValidator.RequestValidateInto(req, medicare)
fmt.Printf("Fourth (as http.Request) ok?: %v\n", ok)
for i, v := range violations {
fmt.Printf("Violation[%d]: Message:%s, Property:%s, Path:%s\n", i+1, v.Message, v.Property, v.Path)
}
}
On go-playground
Disclosure: I am the author of Valix

Issue in Creating JSON output with key:value as tag names using struct

I would want to create an output JSON as shown below,
{
"Name": "John Smith",
"City": "London",
"Contact": [
{ "key": "StreetName", "value": "SomeName" },
{ "key": "PostalCode", "value": "SomeValue" }
],
}
I'm trying to achieve this using the code below,
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name, City string
Contact ContactStruct
}
type ContactStruct struct {
Street, PostalCode map[string]string
}
func main() {
StreetData := make(map[string]string)
StreetData["key"] = "StreetName"
StreetData["value"] = "ABC Street"
PostalCodeData := make(map[string]string)
PostalCodeData["key"] = "PostalCode"
PostalCodeData["value"] = "12345"
jsonData := Person{
Name: "John Smith",
City: "London",
Contact: ContactStruct{
StreetData,
PostalCodeData,
},
}
finalJsonData, err := json.MarshalIndent(jsonData, " ", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(finalJsonData))
}
Below is the output generated using the above code,
{
"Name": "John Smith",
"City": "London",
"Contact": {
"Street": {
"key": "StreetName",
"value": "ABC Street"
},
"PostalCode": {
"key": "PostalCode",
"value": "12345"
}
}
}
Problem: As we can see, output is getting created with the tag names "Street" and PostalCode, because we are creating the JSON using struct value.
I've tried explore various options of using map[string]string and map[string]interface{} inside the Person struct. But it is not working.
Is there any better implementation available to get the JSON output, according to my requirement showed at the beginning of the question.
Thanks in advance for the help. I've started developing in golang recently.
Running Source is available here: https://play.golang.org/p/eIxDyWXfZ1C
May be you want it this way
type Person struct {
Name, City string
Contact []ContactStruct
}
type ContactStruct struct {
Key string
Value string
}
func main() {
StreetData := ContactStruct{Key: "StreetName", Value: "ABC Street"}
PostalCodeData := ContactStruct{Key: "PostalCode", Value: "12345"}
jsonData := Person{
Name: "John Smith",
City: "London",
Contact: []ContactStruct{StreetData, PostalCodeData},
}
finalJsonData, err := json.MarshalIndent(jsonData, " ", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(finalJsonData))
}
Your structure is kind of weird, but this is the solution:
https://play.golang.org/p/CCVuiGd5phq
Quick tip: https://mholt.github.io/json-to-go/ use this page when you need a struct for your json.
You may want to use a custom JSON marshaler for ContactStruct. This allows to keep using a clean Go data type but still implement the specific serialization expected to communicate with the outside world.
View and run on Go playground:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name, City string
Contact ContactStruct
}
type ContactStruct struct {
Street, PostalCode string
}
func appendKV(buf []byte, key string, value string) ([]byte) {
buf = append(buf, []byte(`{"key":`)...)
b, _ := json.Marshal(key)
buf = append(buf, b...)
buf = append(buf, []byte(`,"value":`)...)
b, _ = json.Marshal(value)
buf = append(buf, b...)
buf = append(buf, '}')
return buf
}
func (cs ContactStruct) MarshalJSON() ([]byte, error) {
buf := []byte{'['}
buf = appendKV(buf, "StreetName", cs.Street)
buf = append(buf, ',')
buf = appendKV(buf, "PostalCode", cs.PostalCode)
buf = append(buf, ']')
return buf, nil
}
func main() {
jsonData := Person{
Name: "John Smith",
City: "London",
Contact: ContactStruct{
Street: "ABC Street",
PostalCode: "12345",
},
}
finalJsonData, err := json.MarshalIndent(jsonData, " ", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(finalJsonData))
}

Creating dynamic json

I need to create dynamic json i.e whose key value varies, below mentioned is the json
[{"email":"xxx#gmail.com","location":{"set":"Redmond"},"fname":{"set":"xxxxx"},"clicked_time":{"set":"zz"},"domain":{"add":"ttt"}},{"email":"zzz#gmail.com","location":{"set":"Greece"},"fname":{"set":"zzzzz"},"clicked_time":{"set":"zzz"},"domain":{"add":"zxxxx"}}]
I tried using below code:
rows := []map[string]string{}
if i > 0 {
row := make(map[string]string)
for j:=0;j<len(record);j++ {
key := header[j]
value := record[j]
row[key] = value
}
rows = append(rows, row)
}
How may I add set to location and add to domain to create a nested structure as map can have only one type string or nested structure?
Perhaps I have missed the point a little here, but I am not seeing why this is so dynamic in a way that can't be handled by a struct and the json unmarshal method.
Please see the following for an example
https://play.golang.org/p/8nrO36HQGhy
package main
import (
"encoding/json"
"fmt"
)
type (
Details struct {
Email string `json:"email"`
Location Entry `json:"location"`
FName Entry `json:"fname"`
ClickedTime Entry `json:"clicked_time"`
Domain Entry `json:"domain"`
}
Entry struct {
Set string `json:"set"`
Add string `json:"add"`
}
)
func main() {
d := []byte(`[{
"email": "xxx#gmail.com",
"location": {
"set": "Redmond"
},
"fname": {
"set": "xxxxx"
},
"clicked_time": {
"set": "zz"
},
"domain": {
"add": "ttt"
}
}, {
"email": "zzz#gmail.com",
"location": {
"set": "Greece"
},
"fname": {
"set": "zzzzz"
},
"clicked_time": {
"set": "zzz"
},
"domain": {
"add": "zxxxx"
}
}]`)
x := []Details{}
_ = json.Unmarshal(d, &x)
fmt.Printf("%+v\n", x)
}

Trying to parse the JSON and create an extracted JSON

I am trying to create a JSON object on the fly based on the data received from the API.
Sample Data received: Unmarshalled the data into CiItems struct given below
{
"class_name": "test",
"configuration_items": [
{
"id": "ea09a24f-01ef-42ad-ab19-e0369341d9b3",
"ci_name": "makk",
"comments": null,
"created_by": "mike",
"updated_by": "sam",
"created": "2019-08-02T21:16:35.656Z",
"updated": "2019-08-02T21:21:08.073Z",
"ci_state_id": "randomid",
"super_ci_id": null,
"ci_attributes": [
{
"attribute_id": "c995c693-b97c-4863-a61b-81a5d904c967",
"df_attribute_value": "xsmall",
"attribute_name": "tname",
"data_type": "string"
},
{
"attribute_id": "58845f48-7d2a-4c8c-8591-eaf59a23d84d",
"df_attribute_value": "vmware",
"attribute_name": "provider",
"data_type": "string"
}
]}]}
Below are the structs created:
type Attribute struct {
AttributeID string `json:"attribute_id "`
DfAttributeValue string `json:"df_attribute_value"`
AttName string `json:"attribute_name"`
DataType string `json:"data_type"`
}
// Attributes - array of Attribute
type Attributes []Attribute
// CiItem - confiuraion item of a VM
type CiItem struct {
ID string `json:"ci_id"`
Created string `json:"created"`
Updated string `json:"updated"`
CreatedBY string `json:"created_by"`
UpdatedBY string `json:"updated_by"`
Atts Attributes `json:"ci_attributes"`
}
// CiItems - array of CiItem
type CiItems struct {
ClassName string `json:"class_name"`
Items []CiItem `json:"configuration_items"`
}
Code to unmarshal the data and create a extracted Json:
func (client *Client) GetList() (CiItems, error) {
var out CiItems
err := client.doJsonRequest("GET", &out)
log.Info(out)
if err != nil {
return out, err
}
var output map[string]interface{}
//parseMap(out.Items[0].(map[string]interface{}))
extractBase(out.Items[0], &output)
return out, nil
}
func extractBase(ci interface{}, output interface{}) {
fields := reflect.TypeOf(ci)
values := reflect.ValueOf(ci)
num := fields.NumField()
for i := 0; i < num; i++ {
field := fields.Field(i)
value := values.Field(i)
if string(field.Name) != "Atts" {
name := string(field.Name)
output[name] = string(value)
}
}
}
I am trying to create a JSON with key value of id, ci_name, created_by, updated_by, attribute_name as below
{
"ci_name": "makk",
"created_by": "mike",
"updated_by": "sam",
"created": "2019-08-02T21:16:35.656Z",
"updated": "2019-08-02T21:21:08.073Z",
"tname": "xsmall",
"provider": "vmware"
}
I have tried using reflect and other methods
Create a map using values from CiItem fields and return it from the function.
func extractBase(ci *CiItem) map[string]interface{} {
result := map[string]interface{}{
"ci_name": ci.Name,
"created_by": ci.CreatedBY,
"updated_by": ci.UpdatedBY,
"created": ci.Created,
"updated": ci.Updated,
}
for _, a := range ci.Atts {
result[a.AttName] = a.DfAttributeValue
}
return result
}

Retrieve First Record of Nested Json Array

I'm trying to parse the first record in an embedded JSON array and create an object based on a subset of those properties. I have this working, but based on this question, I have to think there is a more elegant/less brittle way of doing this. For a bit more background, this is a result set from a call to the musicbrainz JSON web service, and I am treating the first artists record as the artist I am looking for.
The format of the JSON is like this:
{
"created": "2014-10-08T23:55:54.343Z",
"count": 458,
"offset": 0,
"artists": [{
"id": "83b9cbe7-9857-49e2-ab8e-b57b01038103",
"type": "Group",
"score": "100",
"name": "Pearl Jam",
"sort-name": "Pearl Jam",
"country": "US",
"area": {
"id": "489ce91b-6658-3307-9877-795b68554c98",
"name": "United States",
"sort-name": "United States"
},
"begin-area": {
"id": "10adc6b5-63bf-4b4e-993e-ed83b05c22fc",
"name": "Seattle",
"sort-name": "Seattle"
},
"life-span": {
"begin": "1990",
"ended": null
},
"aliases": [],
"tags": []
},
...
}
Here's the code I have so far. I'd like to be able to use my ArtistCollection type to get around some of the interface{} stuff, but I'm stuck as to how. I also don't want to bother with mapping all of the properties of the artist record, I'm only interested in the "name" and "id" values.
package main
import (
"fmt"
"encoding/json"
)
type Artist struct {
Id string
Name string
}
type ArtistCollection struct {
Artists []Artist
}
func main() {
raw := //json formatted byte array
var topLevel interface{}
err := json.Unmarshal(raw, &topLevel)
if err != nil {
fmt.Println("Uh oh")
} else {
m := topLevel.(map[string]interface{})
//this seems really hacky/brittle, there has to be a better way?
result := (m["artists"].([]interface{})[0]).(map[string]interface{})
artist := new(Artist)
artist.Id = result["id"].(string)
artist.Name = result["name"].(string)
fmt.Println(artist)
}
}
Requisite go playground link
Define a type that matches the structure of the JSON and unmarshal to a value of that type. I use an anonymous type below. Use an array of length one to grab the first artist record:
package main
import (
"encoding/json"
"fmt"
)
type Artist struct {
Id string
Name string
}
func main() {
raw := // JSON formatted byte array
var result struct {
Artists artist
}
err := json.Unmarshal(raw, &result)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%#v\n", result.Artists[0])
}
playground