Search code examples
jsongostruct

How do I best normalize JSON data to API in Go for a struct


I am quite new to Go and am trying to identify if there is a more succinct way to accomplish normalization of JSON data coming from the front end (JS) to my API. In order to ensure that I am using the correct types when creating a variable from my struct (model.Expense), I am dumping the payload into a map, then normalizing, and saving back to a struct. If someone could school me on a better way of handling this, I would greatly appreciate it! Thanks in advance!

model.Expense struct:

type Expense struct {
    Id        primitive.ObjectID   `json:"_id,omitempty" bson:"_id,omitempty"`
    Name      string               `json:"name"`
    Frequency int                  `json:"frequency"`
    StartDate *time.Time           `json:"startDate"`
    EndDate   *time.Time           `json:"endDate,omitempty"`
    Cost      primitive.Decimal128 `json:"cost"`
    Paid      []string             `json:"paid,omitempty"`
}

Controller in question:

func InsertOneExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Allow-Control-Allow-Methods", "POST")

    var expense map[string]interface{}
    json.NewDecoder(r.Body).Decode(&expense)

    var expenseName string
    if name, ok := expense["name"]; ok {
        expenseName = fmt.Sprintf("%v", name)
    } else {
        json.NewEncoder(w).Encode("missing required name")
    }

    var expenseFrequency int
    if frequency, ok := expense["frequency"]; ok {
        expenseFrequency = int(frequency.(float64))
    } else {
        expenseFrequency = 1
    }

    // Handle startDate normalization
    var expenseStartDate *time.Time
    if startDate, ok := expense["startDate"]; ok {
        startDateString := fmt.Sprintf("%v", startDate)
        startDateParsed, err := time.Parse("2006-01-02 15:04:05", startDateString)

        if err != nil {
            log.Fatal(err)
        }

        expenseStartDate = &startDateParsed
    } else {
        json.NewEncoder(w).Encode("missing required startDate")
    }

    // Handle endDate normalization
    var expenseEndDate *time.Time
    if endDate, ok := expense["endDate"]; ok {
        endDateString := fmt.Sprintf("%v", endDate)
        endDateParsed, err := time.Parse("2006-01-02 15:04:05", endDateString)

        if err != nil {
            log.Fatal(err)
        }

        expenseEndDate = &endDateParsed
    } else {
        expenseEndDate = nil
    }

    // Handle cost normaliztion
    var expenseCost primitive.Decimal128
    if cost, ok := expense["cost"]; ok {
        costString := fmt.Sprintf("%v", cost)
        costPrimitive, err := primitive.ParseDecimal128(costString)

        if err != nil {
            log.Fatal(err)
        }

        expenseCost = costPrimitive
    } else {
        json.NewEncoder(w).Encode("missing required cost")
        return
    }

    normalizedExpense := model.Expense{
        Name:      expenseName,
        Frequency: expenseFrequency,
        StartDate: expenseStartDate,
        EndDate:   expenseEndDate,
        Cost:      expenseCost,
    }

    // Do more things with the struct var...
}

Solution

  • You can define the json.UnmarshalJSON interface and then manually validate data however you need. Try something like this:

    package main
    
    import (
        "encoding/json"
        "fmt"
        "strconv"
    )
    
    type CoolStruct struct {
        MoneyOwed string `json:"money_owed"`
    }
    
    // UnmarshalJSON the json package will delegate deserialization to our code if we implement the json.UnmarshalJSON interface
    func (c *CoolStruct) UnmarshalJSON(data []byte) error {
        // get the body as a map[string]*[]byte
        raw := map[string]*json.RawMessage{}
        if err := json.Unmarshal(data, &raw); err != nil {
            return fmt.Errorf("unable to unmarshal raw meessage map: %w", err)
        }
    
        // if we don't know the variable type sent we can unmarshal to an interface
        var tempHolder interface{}
        err := json.Unmarshal(*raw["money_owed"], &tempHolder)
        if err != nil {
            return fmt.Errorf("unable to unmarshal custom value from raw message map: %w", err)
        }
    
        // the unmarshalled interface has an underlying type use go's typing
        // system to determine type conversions / normalizations required
        switch tempHolder.(type) {
        case int64:
            // once we determine the type of the we just assign the value
            // to the receiver's field
            c.MoneyOwed = strconv.FormatInt(tempHolder.(int64), 10)
        // we could list all individually or as a group; driven by requirements
        case int, int32, float32, float64:
            c.MoneyOwed = fmt.Sprint(tempHolder)
        case string:
            c.MoneyOwed = tempHolder.(string)
        default:
            fmt.Printf("missing type case: %T\n", tempHolder)
        }
        // success; struct is now populated
        return nil
    }
    
    func main() {
        myJson := []byte(`{"money_owed": 123.12}`)
        cool := CoolStruct{}
        // outside of your struct you marshal/unmarshal as normal
        if err := json.Unmarshal(myJson, &cool); err != nil {
            panic(err)
        }
        fmt.Printf("%+v\n", cool)
    }
    

    Output: {MoneyOwed:123.12}
    Playground link: https://go.dev/play/p/glStUbwpCCX