NeuroAgent

How to Exclude Fields from JSON in Go: A Complete Guide

Learn why the json:"-" tag doesn't work with embedded structs in Go and how to properly exclude fields from JSON output. Complete guide with code examples.

How do I exclude a field when converting a struct to JSON in Go?

I have the following structs:

go
type Object struct {
    UpdatedAt time.Time
}

type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price int
}

When running the code:

go
myOb := MyObject{}
myOb.UpdatedAt = time.Now()
myOb.Price = 100
json.Marshal(myOb)

I get the following result:

json
{
    "UpdatedAt": "0001-01-01T00:00:00Z",
    "Price": 100
}

How do I properly exclude the UpdatedAt field from the JSON output? Why is the field with the json:"-" tag still included in the output?

NeuroAgent

Excluding the UpdatedAt Field from JSON in Go

To exclude the UpdatedAt field from JSON when converting a struct in Go, you need to use the correct combination of tags and methods. In your case, the problem occurs due to field resolution in embedded structs and the order in which they are processed.


Table of Contents


Main Problem and Solution

In your code, the problem occurs because Go processes the embedded Object struct containing the UpdatedAt field first during JSON marshaling. Even though your local UpdatedAt field has the json:"-" tag, the embedded struct has already added its field to the result.

The correct solution is to explicitly specify which field should be used or modify the structure:

go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"` // This field will be ignored
    Price     int
}

To solve your problem, use one of these approaches:

  1. Create an alias for the embedded field:
go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
    // Explicitly specify which field should be in JSON
    UpdatedAtField time.Time `json:"updated_at"`
}
  1. Use the json.Marshaler interface for complete control over the serialization process.

How Field Resolution Works in Go

According to the Go documentation, Go uses the following rules for field resolution during JSON marshaling:

  1. If there are multiple fields at the same level and only one has an explicit JSON name, that field takes priority and the others are excluded
  2. For embedded structs, Go similarly applies field visibility rules
  3. Processing order is important - embedded structs are processed before fields of the main type

This explains why your json:"-" tag doesn’t work as expected - the embedded Object struct has already added its UpdatedAt field before the system processes the tag on your local field.

Ways to Exclude Fields from JSON

1. Using the json:"-" Tag

The basic way to exclude a field from JSON output:

go
type User struct {
    ID       int    `json:"id"`
    Password string `json:"-"` // This field will never be in JSON
}

2. Using omitempty

Exclude a field only when it’s absent or has a zero value:

go
type Product struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price,omitempty"`
    Discount float64 `json:"discount,omitempty"`
}

3. Reflection for Selective Marshaling

As mentioned in this Stack Overflow answer, you can use the reflect package for selective field inclusion:

go
func MarshalSelective(v interface{}, fieldsToInclude []string) ([]byte, error) {
    val := reflect.ValueOf(v)
    // Logic for selective field inclusion
}

4. Custom Marshaling Methods

Implement the json.Marshaler interface:

go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
}

func (m MyObject) MarshalJSON() ([]byte, error) {
    type Alias MyObject
    return json.Marshal(struct {
        Alias
        UpdatedAt time.Time `json:"updated_at"`
    }{
        Alias:     (Alias)(m),
        UpdatedAt: m.UpdatedAt,
    })
}

Advanced Techniques for JSON Output Control

1. Using Composition with Custom Types

As described in the Boldly Go article, you can create a local type with the required fields:

go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
}

// For marshaling, create only the needed fields
type MyObjectMarshal struct {
    UpdatedAt time.Time `json:"updated_at"`
    Price     int       `json:"price"`
}

func (m *MyObject) MarshalJSON() ([]byte, error) {
    return json.Marshal(MyObjectMarshal{
        UpdatedAt: m.UpdatedAt,
        Price:     m.Price,
    })
}

2. Managing Field Name Conflicts

When fields with the same name exist in different structs, you can use aliases:

go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
    UpdatedAtFromObject time.Time `json:"updated_at_from_object"`
}

3. Using Libraries for JSON Control

There are third-party libraries that provide more flexible JSON output control, such as:

  • https://github.com/mitchellh/mapstructure
  • https://github.com/fatih/structs

Code Examples and Best Practices

Example 1: Proper Handling of Embedded Structs

go
type Object struct {
    UpdatedAt time.Time
}

type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
}

// Proper usage
func main() {
    myOb := MyObject{}
    myOb.UpdatedAt = time.Now()
    myOb.Price = 100
    
    // Explicitly specify which field should be in JSON
    type Alias MyObject
    result, _ := json.Marshal(struct {
        Alias
        UpdatedAt time.Time `json:"updated_at"`
    }{
        Alias:     (Alias)(myOb),
        UpdatedAt: myOb.UpdatedAt,
    })
    
    fmt.Println(string(result))
}

Example 2: Fully Custom Marshaling

go
type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
}

func (m MyObject) MarshalJSON() ([]byte, error) {
    type Alias MyObject
    data := struct {
        Alias
        UpdatedAt time.Time `json:"updated_at,omitempty"`
        Price     int       `json:"price"`
    }{
        Alias:     (Alias)(m),
        UpdatedAt: m.UpdatedAt,
        Price:     m.Price,
    }
    return json.Marshal(data)
}

Example 3: Using Interfaces for Flexibility

go
type JSONExclude interface {
    ShouldExclude(fieldName string) bool
}

type MyObject struct {
    Object
    UpdatedAt time.Time `json:"-"`
    Price     int
}

func (m MyObject) ShouldExclude(fieldName string) bool {
    return fieldName == "UpdatedAt"
}

func MarshalWithExclude(v interface{}) ([]byte, error) {
    val := reflect.ValueOf(v)
    // Implementation of custom marshaling with exclusion checks
}

Conclusion

Excluding fields from JSON output in Go requires understanding how embedded structs are processed and how field name conflicts are resolved. The main takeaways are:

  1. The json:"-" tag only works for fields that don’t conflict with fields from embedded structs
  2. For managing complex structs, use custom MarshalJSON methods
  3. When working with embedded structs, always explicitly specify which fields should be included in the JSON
  4. For complex scenarios, consider using reflection or third-party libraries

The recommended practice for your case is to implement a custom MarshalJSON method that will explicitly control which fields are included in the JSON output. This gives you complete control over the serialization process and helps avoid unexpected behavior when working with embedded structs.

Sources

  1. Go JSON package documentation - encoding/json/v2
  2. Stack Overflow - Removing fields from struct or hiding them in JSON Response
  3. Boldly Go - JSON Tricks: Extending an Embedded Marshaler
  4. GitHub Issue - encoding/json: tag json:"-" doesn’t hide an embedded field
  5. Bruno Scheufler - Go Embeds and JSON
  6. Gopher Dojo - Go JSON (Un)Marshalling, Missing Fields and Omitempty