Skip to main content

Get the Reddit app

Scan this QR code to download the app now
Or check it out in the app stores
r/golang icon
r/golang icon
Go to golang
r/golang
A banner for the subreddit

Ask questions and post articles about the Go programming language and related tools, events etc.


Members Online

JSON response transform techniques

help

I'm new to Go and still learning but this is something I've not found an answer for. Let's say I have a model with various properties such as name, email, ID, password etc and I want to return this object as a JSON response. I know in this instance I would set the json attributes on the model struct and use json.Marshal to produce the output.

But, what if certain responses only require certain properties to be returned so one endpoint might need to return a list of these models but only email and ID and another would be only name and email. This is obviously a basic example but in Laravel I would use a Resource and output only the required attributes for the endpoint.

Is there a "Go" way to do this or just a case of creating a second struct with only the desired properties or even creating a map with the required properties? I've done the latter but it feels like I'm missing a better way.

Share
Sort by:
Best
Open comment sort options
u/jerf avatar
Edited

The way I'd go with is struct composition.

type UserID struct {
    UserID int
    Email string
}

type ModificationTimes struct {
    Created time.Time
    LastModified time.Time
}

type PostContent struct {
    Content string
    Attachments []Attachments
}

type Post struct {
    UserID
    ModificationTimes
    PostContent
}

In your example you mention one having email and ID and another having email and name. In my experience, somewhat to my surprise, I have found that typically in real life the things you want to separate like this do not overlap and you don't get problems with that sort of intersection. With the possible exception of an ID, which you can just then create more types that blend something with an ID if you need it. Types are cheap, don't be afraid to create many of them.

You may then notice that these things are actually their separate entities and you may start finding it useful to put methods on them, too.

Just watch the composition rules for marshaling method implementations; if you have

func (uid UserID) UnmarshalJSON([]byte) error {
    // code here
}

on the types as shown above, when trying to unmarshal a Post Go will find the UserID's UnmarshalJSON implementation and you may pull out your hair trying to figure out how to unmarshal the top-level with "normal" unmarshalling. The secret is something like

type MarshalPost Post

then you can unmarshal a *MarshalPost wrapped around a *Post and you'll get the "default" JSON unmarshaling behavior. That's a json.Unmarshal(b, (*MarshalPost)(&post)).

Oh. I hadn’t considered this pattern. However, I don’t think I quite understood your last point about the top level struct. I guess I have to try it out and see.

u/jerf avatar

Yes, sorry, I was writing that in a break and then a meeting occurred. It doesn't help that I was wrong, which is what I get for being in a hurry. The trick I referred to can easily strip an Unmarshal function off of a particular struct, but not embedded ones. You can adapt it obviously but it will be more work. I don't see a way to "skip" an embedded struct from taking over an unmarshal function.

(Also screwing me up is that the YAML library, which I've used a lot lately, does have a way to do this. I prefer how it does unmarshaling, by giving you access to the intermediate parse tree, but it has performance implications.)

More replies
More replies

This article walks through various techniques for defining and omitting optional fields:

https://emretanriverdi.medium.com/json-serialization-in-go-a27aeeb968de

Thanks, I've actually read that already but I missed out a crucial detail in my question. My model is being loaded from a database using gorm so some of the fields will have values but I still want to omit them but only for some requests. I'm thinking maybe a function which takes a struct and a list of fields to output might be a good way?

u/RiotBoppenheimer avatar

I would really avoid having your display logic mixed with your database logic. You will end up in a position where you want to change how something is displayed independent of how it is stored and it is very cumbersome to make this change if you tie these layers together.

It's verbose but the way I've usually handled this is to have a separate struct that contains all the fields I want to display to the end-user and have a function which will transform one (or several!) database models into this one view model.

That's basically what I'm trying to do but wording it badly! Wondering if there's a given way to do it in Go. In Laravel it would be a resource which separates the model from the output and I guess I'm looking for the equivalent. The way you suggested is what I've ended up doing but making sure I've not missed a better way

u/RiotBoppenheimer avatar

In general in Go the simplest way is often the way to go. And by simple here, I mean "lowest tech", not "lowest lines of code".

more reply More replies
More replies
More replies
More replies
More replies

What you are seeking is called a "data transfer object" (DTO).

You can define separate DTOs for each endpoint and convert your database model instances to those DTOs. So your database models do not even care about JSON marshalling tags.

This method works particularly well for API backwards compatibility.

I haven’t done anything like this, but my first instinct would be to look into custom struct tags. I believe you can use them to implement custom de/serialization logic.

Another option might be implementing the un/marshal interface (I forgot what it’s called) for your struct. But I feel like this is probably not the way to go about it.

I've had a read of the various techniques and decided the best fit and the most secure as there's no possibility of leaking future information inadvertently is to have separate DTO response objects and use a NewXXX() function to write them from the DB models. It seemed a bit verbose initially but I'm still getting used to the more simple nature of Go and having written a few last night it actually works really well. Thanks for your comments and suggestions!