Question

Dynamic JSON Fields in Go: Hiding Struct Fields in API Responses

gojson

Question

In a Go API, I query data, create a struct instance, and encode that struct as JSON for the response.

I want the client to be able to choose which fields are returned by passing a fields query parameter in the request. Depending on the fields value, the response should include only specific struct fields.

Is it possible to remove fields from a struct at runtime, or at least hide them dynamically in the JSON response?

omitempty is not enough here, because some fields may contain valid empty values and still need to be excluded based on the client's request.

Here is a simplified version of the structs:

type SearchResult struct {
    Date        string      `json:"date"`
    IdCompany   int         `json:"idCompany"`
    Company     string      `json:"company"`
    IdIndustry  interface{} `json:"idIndustry"`
    Industry    string      `json:"industry"`
    IdContinent interface{} `json:"idContinent"`
    Continent   string      `json:"continent"`
    IdCountry   interface{} `json:"idCountry"`
    Country     string      `json:"country"`
    IdState     interface{} `json:"idState"`
    State       string      `json:"state"`
    IdCity      interface{} `json:"idCity"`
    City        string      `json:"city"`
}

type SearchResults struct {
    NumberResults int            `json:"numberResults"`
    Results       []SearchResult `json:"results"`
}

The response is encoded like this:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)

What is the best way to support dynamic field selection in this situation?

Short Answer

By the end of this page, you will understand why Go structs cannot have fields removed at runtime, and how to return only selected JSON fields anyway. You will learn practical ways to build dynamic API responses using maps, filtered objects, and custom JSON handling.

Concept

In Go, a struct has a fixed shape defined at compile time. That means you cannot add or remove fields from a struct instance dynamically after the program is built.

However, JSON output is more flexible than the struct itself. Even if your Go struct always has the same fields, you can choose what to include in the JSON you send to the client.

This matters in real APIs because clients often want:

  • smaller responses
  • fewer unnecessary fields
  • faster network transfers
  • more control over what data they receive

There are three common ways to handle this in Go:

  1. Encode a map[string]interface{} instead of the struct

    • Build only the keys the client asked for.
    • Good for highly dynamic output.
  2. Create a filtered response object

    • Convert your struct into a map or another struct before encoding.
    • Good when you want more control and validation.
  3. Implement custom JSON marshaling

    • Advanced option using MarshalJSON().
    • Useful if the filtering logic belongs closely to the type.

The key idea is this:

  • The struct stays fixed in Go
  • The JSON response can still be dynamic

So the problem is not changing the struct itself. The problem is choosing a different JSON representation before encoding.

Mental Model

Think of a Go struct like a printed paper form with fixed boxes:

  • one box for date
  • one box for company
  • one box for country
  • and so on

You cannot tear boxes off the form at runtime.

But JSON output is like copying only some boxes from that form onto a new sheet before handing it to someone.

So instead of changing the original form, you create a custom outgoing version that includes only the requested fields.

Take Quiz

Syntax and Examples

Fixed struct, dynamic JSON

A Go struct is fixed:

type SearchResult struct {
    Date    string `json:"date"`
    Company string `json:"company"`
    Country string `json:"country"`
}

You cannot do this:

// Not possible in Go:
// remove the Country field from result at runtime

But you can build a map dynamically:

result := SearchResult{
    Date:    "2024-01-01",
    Company: "Acme",
    Country: "USA",
}

response := map[string]interface{}{}
response["date"] = result.Date
response["company"] = result.Company

json.NewEncoder(w).Encode(response)

Output:

{
  "date": "2024-01-01",
  "company": "Acme"
}

Example: filter by fields

Suppose the client sends:

?fields=date,company

You can parse that and keep only the requested fields:

func filterResult(r SearchResult, fields map[string]bool) map[string]interface{} {
    out := map[string]interface{}{}

    if fields["date"] {
        out["date"] = r.Date
    }
    if fields["company"] {
        out["company"] = r.Company
    }
    if fields["country"] {
        out["country"] = r.Country
    }

    return out
}

This is beginner-friendly, explicit, and easy to debug.

Filtering a list of results

results := []SearchResult{
    {Date: "2024-01-01", Company: "Acme", Country: "USA"},
    {Date: "2024-01-02", Company: "Beta", Country: "Canada"},
}

fields := map[string]bool{
    "date":    true,
    "company": true,
}

filtered := make([]map[string]interface{}, 0, len(results))
for _, r := range results {
    filtered = append(filtered, filterResult(r, fields))
}

json.NewEncoder(w).Encode(map[string]interface{}{
    "numberResults": len(filtered),
    "results":       filtered,
})

This produces a response where each item contains only the selected keys.

Step by Step Execution

Consider this example:

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

type SearchResult struct {
    Date    string `json:"date"`
    Company string `json:"company"`
    Country string `json:"country"`
}

func parseFields(raw string) map[string]bool {
    fields := map[string]bool{}
    for _, f := range strings.Split(raw, ",") {
        f = strings.TrimSpace(f)
        if f != "" {
            fields[f] = true
        }
    }
    return fields
}

func filterResult(r SearchResult, fields map[string]bool) map[string]interface{} {
    out := map[string]interface{}{}
    if fields["date"] {
        out["date"] = r.Date
    }
    if fields["company"] {
        out["company"] = r.Company
    }
    if fields["country"] {
        out["country"] = r.Country
    }
    return out
}

func main() {
    result := SearchResult{
        Date:    "2024-01-01",
        Company: "Acme",
        Country: "USA",
    }

    fields := parseFields("date,company")
    filtered := filterResult(result, fields)

    b, _ := json.Marshal(filtered)
    fmt.Println(string(b))
}

What happens step by step

  1. parseFields("date,company") is called.
  2. The string is split into "date" and "company".
  3. A map is built:
map[string]bool{
    "date": true,
    "company": true,
}
  1. filterResult() checks each possible field.
  2. date is requested, so it is added.
  3. company is requested, so it is added.
  4. country is not requested, so it is skipped.
  5. The final map becomes:
map[string]interface{}{
    "date": "2024-01-01",
    "company": "Acme",
}
  1. json.Marshal() turns that map into JSON.
  2. The printed result is:
{"company":"Acme","date":"2024-01-01"}

Note that JSON object key order is not guaranteed when using maps.

Real World Use Cases

Dynamic field selection is useful in many real systems:

Public APIs

Clients may want only a few fields to reduce payload size.

Example:

GET /companies?fields=idCompany,company,country

Mobile apps

Mobile clients often need smaller responses to save bandwidth and improve performance.

Admin dashboards

One screen may need only summary fields, while another needs full detail.

Data export tools

Users may choose specific columns for CSV-like or JSON exports.

Microservices

One service may call another and request only the fields needed for a workflow.

Privacy and access control

Some fields can be excluded based on permissions, role, or subscription level.

Field filtering is not only about convenience. It can also improve efficiency and safety.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually avoid trying to mutate struct definitions. Instead, they use a few common patterns.

Pattern 1: Build a response DTO

A DTO (Data Transfer Object) is a type or structure created specifically for output.

  • internal model stays complete
  • outgoing response is shaped for the API

Pattern 2: Use a whitelist of allowed fields

Never trust arbitrary field names from the client without validation.

var allowedFields = map[string]bool{
    "date":       true,
    "idCompany":  true,
    "company":    true,
    "idIndustry": true,
    "industry":   true,
    "country":    true,
}

Then accept only known fields.

Pattern 3: Early validation

If the client asks for an unknown field, return a clear error early.

if !allowedFields[field] {
    return errors.New("invalid field: " + field)
}

Pattern 4: Filter after fetching data

Often, developers query full rows from a database and then filter JSON output afterward.

This is simpler and safer for beginners.

Pattern 5: Push selection down to the database

In larger systems, the selected fields may also affect the SQL query. That can improve performance, but it adds complexity and must be done carefully.

Pattern 6: Guard clauses

If no fields parameter is provided, many APIs return the default full response.

if rawFields == "" {
    json.NewEncoder(w).Encode(fullResponse)
    return
}

This keeps the normal path simple.

Common Mistakes

1. Expecting omitempty to solve field selection

omitempty only removes fields when they have zero values.

type User struct {
    Name string `json:"name,omitempty"`
}

If Name is an empty string, it may be omitted. But if the client does not want name, omitempty does not help unless the value also happens to be empty.

2. Trying to remove struct fields at runtime

Broken idea:

// This is not possible in Go.
// result.RemoveField("country")

Go structs are static.

3. Returning unknown fields without validation

Broken example:

fields := strings.Split(raw, ",")
// blindly trust every field name

This can cause inconsistent behavior and make your API harder to maintain.

Better:

if !allowedFields[f] {
    return fmt.Errorf("invalid field: %s", f)
}

4. Using reflection too early

Reflection can inspect struct fields dynamically, but it is more complex and easier to get wrong.

For beginners, explicit if fields["name"] { ... } logic is usually better.

5. Forgetting that map key order is not guaranteed

If you encode a map[string]interface{}, the JSON key order may vary.

That is usually fine, because JSON consumers should not rely on order.

6. Mixing internal model and API response too tightly

Your database or business logic model does not have to match the exact JSON shape you return.

Separating them often makes the code easier to maintain.

Comparisons

ApproachBest forProsCons
Fixed struct with tagsNormal static APIsSimple, type-safe, easy to readCannot dynamically exclude arbitrary fields based on request
omitemptyHiding zero-value fieldsVery easy to useNot for client-driven field selection
map[string]interface{}Dynamic responsesFlexible, easy to filterLess type-safe, more manual code
Custom response structControlled API outputClear structure, easier to testCan require many variants
MarshalJSON()Reusable custom encoding logicCentralized JSON behaviorMore advanced, can be harder to follow
Reflection-based filteringGeneric field selection utilitiesCan reduce repetitive codeMore complexity, less beginner-friendly

omitempty vs dynamic filtering

FeatureomitemptyDynamic filtering
Based on zero valueYesNo
Based on client fields parameterNoYes
Works with empty but valid valuesNoYes
Easy for static APIsYesSometimes

For your case, dynamic filtering is the correct idea because inclusion depends on the request, not on whether a value is empty.

Take Quiz

Cheat Sheet

Quick rules

  • Go structs are fixed at compile time.
  • You cannot remove struct fields at runtime.
  • You can choose which fields appear in JSON.
  • omitempty hides zero values, not request-selected fields.
  • For dynamic API fields, use a filtered map[string]interface{} or a custom response type.

Common pattern

fields := map[string]bool{
    "date": true,
    "company": true,
}

out := map[string]interface{}{}
if fields["date"] {
    out["date"] = result.Date
}
if fields["company"] {
    out["company"] = result.Company
}

Parse fields parameter

func parseFields(raw string) map[string]bool {
    out := map[string]bool{}
    for _, f := range strings.Split(raw, ",") {
        f = strings.TrimSpace(f)
        if f != "" {
            out[f] = true
        }
    }
    return out
}

Validate allowed fields

var allowed = map[string]bool{
    "date": true,
    "company": true,
    "country": true,
}

Return full response when no fields are provided

if rawFields == "" {
    json.NewEncoder(w).Encode(fullResponse)
    return
}

Edge cases

  • Empty fields string
  • Unknown field names
  • Duplicate field names
  • Map JSON key order is not guaranteed
  • Empty values may still need to be included if requested

FAQ

Can I remove a field from a Go struct at runtime?

No. Struct fields are fixed when the program is compiled.

How do I hide fields in JSON based on a request parameter?

Build a new value for output, usually a map[string]interface{} or a custom response type containing only the requested fields.

Why is omitempty not enough?

Because omitempty depends on whether a value is empty, not on whether the client asked for the field.

Should I use reflection for this?

Usually not at first. Reflection can work, but explicit filtering code is simpler and easier to maintain.

Is using map[string]interface{} a good idea in APIs?

Yes, when the response shape is dynamic. Just validate input fields and keep the filtering logic clear.

What should happen if the client requests an invalid field?

A common approach is to return a 400 Bad Request with a message listing the invalid field.

Should I filter at the JSON layer or in the database query?

For simple implementations, filtering JSON output is fine. For performance-sensitive systems, you may later optimize the database query too.

Take Quiz

Mini Project

Description

Build a small Go HTTP endpoint that returns company search results and supports a fields query parameter. The endpoint should always use the same Go struct internally, but dynamically return only the requested JSON fields. This demonstrates a practical pattern for real APIs where clients want control over the response payload.

Goal

Create an endpoint that accepts ?fields=... and returns only those fields in each result item.

Requirements

[ "Define a SearchResult struct with several JSON fields.", "Accept an optional fields query parameter containing comma-separated field names.", "Validate requested fields against an allowed list.", "Return the full response if no fields parameter is provided.", "Return filtered JSON objects when fields are specified." ]

Take Quiz

Keep learning

Related questions

View more