sat
sat

Reputation: 5709

golang - Rest update request using http patch semantics

type A struct {
    Id int64
    Email sql.NullString
    Phone sql.NullString
}

Assume I have one record in the database

A{1, "[email protected]", "1112223333"}

Send an update request via PUT

curl -X PUT -d '{"Email": "[email protected]", "Phone": null}' http://localhost:3000/a/1 

Here is the psuedo algorithm that would work with a full PUT request (i.e. update all fields of the record A - but it will cause difficulties with the PATCH request semantics - delta update)

-- Unmarshal json into empty record

  a := A{}
  json.Unmarshal([]byte(request.body), &a)

-- Load the record from the database

aFromDb = <assume we got record from db> //A{1, "[email protected]", "1112223333"}

-- Compare a and aFromDB

-- Notice the email change and set it on aFromDb - ok

-- Notice the phone number change -- but wait! Was it set to NULL in the JSON explicitly or was it not even included in the JSON? i.e. was the json request - {"Email": "[email protected]", "Phone": null} or was it {"Email": "[email protected]"}?

How can we tell by just looking at the unmarshaled json into the struct a?

Is there another method to do the update via rest (with patch semantics)? I am looking for a generic way to do it (not tied to a particular struct).

Upvotes: 3

Views: 3017

Answers (3)

TaylorBarrick
TaylorBarrick

Reputation: 85

You could potentially write your own marshalling/uinmarshalling of your struct and react to the raw response within, although it might be non-obvious witgh inspection what that those functions are manipulating.

Alternatively, you could not omitempty within your fields and force null populated fields.

Or, maybe leveraging a different flavor of patching, perhaps http://jsonpatch.com/, which is more explicit in the nature of your modifications. This would require the client to be more understanding of the state of changes than say for a put.

Upvotes: 0

seong
seong

Reputation: 1996

I created a separate datatype for this purpose. This example is for an int64 (actually string-encoded int64), but you can easily change it to a string as well. The idea behind it is, that the UnmarshalJSON method will only be called if the value is present in the JSON. The type will implement the Marshaler and the Unmarshaler.

// Used as a JSON type
//
// The Set flag indicates whether an unmarshaling actually happened on the type
type RequiredInt64 struct {
    Set   bool
    Int64 int64
}

func (r RequiredInt64) MarshalJSON() ([]byte, error) {
    lit := strconv.FormatInt(r.Int64, 10)
    return json.Marshal(lit)
}

func (r *RequiredInt64) UnmarshalJSON(raw []byte) error {
    var lit string
    var err error
    if err = json.Unmarshal(raw, &lit); err != nil {
        return err
    }
    r.Int64, err = strconv.ParseInt(lit, 10, 64)
    if err != nil {
        return err
    }
    r.Set = true
    return nil
}

So, if Set is false, you know that the value was not present in the JSON.

Upvotes: 1

fabmilo
fabmilo

Reputation: 48330

Try adding this tags to the struct:

type A struct {
    Id int64 `json:"Id,omitempty"`
    Email sql.NullString `json:"Email,omitempty"`
    Phone sql.NullString `json:"Phone,omitempty"`
}

In this way if you are serializing and the field is empty then the json will not contain the field.

When deserializing though the field will have a either a value or it will have the default value for the type (Nil for the pointer or empty string for strings).

Upvotes: 0

Related Questions