I am writing an API that is used to perform basic CRUD operations (struct <=> mysql table basically). Here is an example struct that maps to a table in my database. I use pointers for the fields so that I can treat nil
as NULL/absent:
type Foo struct {
Id *int32
Name *string
Description *string
CreateDate *string
}
The Id
field is an autoincrement field that should be assigned by the database. The Name
field is writable and required. The Description
field is writable and nullable. The CreateDate
field is assigned by MySQL on insert and should not be written to.
When the user POSTs a new Foo to create, the request body looks like this in JSON:
POST /Foo
{"Name": "test name", "Description": "test description"}
It's easy to decode this and hydrate a Foo struct using a
foo := Foo{}
json.NewDecoder(requestBody).Decode(&foo)
I'm using the https://github.com/coopernurse/gorp library to simplify inserts/updates/deletes, but my issue still holds even if I'm writing raw sql if I wish to generalize the query creation using reflection on fields.
gorpDbMap.Insert(&foo)
My first problem arises if the user provides an read-only field. If this request body is POSTed, the struct happily accepts Id
and when I do the insert it overrides the autoincrement value. I know this is somewhat my fault for using an ORM rather than manually writing a SQL insert
but my hope was that I could in some way enforce when hydrating the struct that only those writable fields should be decoded and any others ignored (or causing an error):
POST /Foo
{"Id": 1, "Name": "test name"}
I cannot find a simple way other than manually examining the hydrated struct and unsetting any read-only fields that I didn't want the user to provide.
The second problem I am experiencing is determining when a user is unsetting a value (passing NULL
for a field to update) vs when the value was not provided. This is a partial update/PATCH in RESTful terminology.
For example, suppose a Foo with id=1 exists. The user now wishes to update the Name
from test name
to new name
and the Description
from test description
to NULL
.
PATCH /Foo/1
{"Name": "new name", "Description": NULL}
Since my struct uses pointers for its fields I can determine if the Description
should be set to null on create if foo.Description == nil
. But in this partial update, how can I differentiate between the case where Description
was not provided (and should thus be left as-is) and the case above where the caller wishes to set the value of Description
to NULL
?
I know there are ways to solve this by writing a lot of custom code around each struct I define, but I was hoping for a general solution that doesn't require so much boilerplate. I've also seen solutions that adopt a different body format for PATCH requests, but I have to meet the existing contract so I cannot adopt a different format for partial updates.
I'm considering a couple options but neither satisfy me.
Use
interface
-typed maps and write code to examine each field and assert types as necessary. This way I can determine if a field andNULL
vs not provided at all. Seems like a lot of work.Define multiple structs for each scenario. This feels a little cleaner, but also a little unnecessarily verbose. And it only resolves one of the two problems I have (enforcing read-only) but not determining when a NULLable field is actually nulled out on partial update or just not provided.
e.g.
type FooWrite struct {
Name *string
Description *string
}
type FooRead struct {
FooWrite
Id int32
CreateDate string
}
This article addresses part of the issue and got me this far, but doesn't address the two problems I'm having now: https://willnorris.com/2014/05/go-rest-apis-and-pointers
Most suggestions I've seen revolve around changing the design of my schema and avoiding NULLs, but I do not have the ability to modify that as it is already in use by other consumers.