Question

Go Struct Tags: Use BSON and JSON Field Names Together

gojsonstruct

Question

I retrieve an item from MongoDB using this Go struct:

type Page struct {
    PageId string                 `bson:"pageId"`
    Meta   map[string]interface{} `bson:"meta"`
}

I also need to encode this value as JSON. Without JSON tags, the output uses exported Go field names such as PageId instead of pageId.

How can I define both BSON and JSON field-name tags on the same struct field? I tried separating them with a comma, but it did not work:

type Page struct {
    PageId string                 `bson:"pageId",json:"pageId"`
    Meta   map[string]interface{} `bson:"meta",json:"pageId"`
}

Short Answer

Go struct tags can contain metadata for more than one package. You will learn the correct syntax for combining bson and json tags, how tag options work, and how to avoid common field-name mistakes when moving data between MongoDB and JSON APIs.

Concept

A struct tag is text attached to a Go struct field. Packages use that text as metadata when converting a struct to or from another format.

For example:

  • MongoDB BSON libraries commonly read the bson tag.
  • Go's standard encoding/json package reads the json tag.

A single field can have multiple tags because each tag has its own key. Tags must be written inside one backtick-delimited string, with tag entries separated by spaces:

`bson:"pageId" json:"pageId"`

A comma does not separate tag keys. Instead, commas belong inside the value of one tag to specify options such as omitempty:

`json:"pageId,omitempty"`

This matters because Go field names must be exported (start with an uppercase letter) for encoders to access them, while database documents and API payloads often use lower camel case, snake case, or another external naming convention.

Mental Model

Think of a struct field as a person who has different name badges for different places.

  • In Go code, the field is named PageId so it is exported and accessible.
  • At MongoDB's entrance, it wears the bson:"pageId" badge.
  • At a JSON API's entrance, it wears the json:"pageId" badge.

Both badges are attached to the same field, but each system reads only the badge intended for it. Put the badges side by side with a space, not a comma.

Take Quiz

Syntax and Examples

The general syntax is:

type TypeName struct {
    FieldName FieldType `packageName:"externalName" anotherPackage:"externalName"`
}

For BSON and JSON, write one tag string and separate the tag entries with spaces:

package main

import (
    "encoding/json"
    "fmt"
)

type Page struct {
    PageID string                 `bson:"pageId" json:"pageId"`
    Meta   map[string]interface{} `bson:"meta" json:"meta"`
}

func main() {
    page := Page{
        PageID: "home",
        Meta: map[string]interface{}{
            "title": "Welcome",
        },
    }

    data, err := json.Marshal(page)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(data))
}

Output:

{"pageId":"home","meta":{"title":"Welcome"}}

PageID is a common Go spelling for an initialism such as ID, but PageId also works. The tag controls the external name in either case.

The Meta field must use json:"meta", not json:"pageId". Each field should normally have its own appropriate external name.

Step by Step Execution

Consider this code:

page := Page{
    PageID: "about",
    Meta: map[string]interface{}{
        "published": true,
    },
}

data, err := json.Marshal(page)

When json.Marshal runs:

  1. It examines the exported PageID field.
  2. It finds json:"pageId" in that field's tag.
  3. It writes the JSON key as "pageId", not "PageID".
  4. It reads the value "about" and writes it as the value of that key.
  5. It examines Meta and finds json:"meta".
  6. It converts the map to a JSON object.
  7. data contains JSON equivalent to:
{"pageId":"about","meta":{"published":true}}

If the program instead used a MongoDB BSON encoder or decoder, that package would look for bson:"pageId" and bson:"meta" on the same fields.

Real World Use Cases

  • MongoDB-backed REST APIs: Read a document with BSON names and send a JSON response using the same or different API names.
  • Data migration tools: Decode old documents using legacy BSON field names while producing a new JSON export format.
  • Configuration services: Store settings in MongoDB and expose selected settings through JSON endpoints.
  • Event publishing: Read application data from a database, then serialize it as JSON for a queue, webhook, or log stream.
  • Third-party integrations: Keep idiomatic exported Go field names while matching another service's exact JSON naming rules.
Take Quiz

Real Codebase Usage

In production code, structs often serve one boundary only: a database model, an API request, or an API response. If one struct is used at multiple boundaries, multiple tags are common.

Typical shared model

type Page struct {
    PageID string                 `bson:"pageId" json:"pageId"`
    Meta   map[string]interface{} `bson:"meta" json:"meta"`
}

Omitting empty optional fields

Use omitempty as an option within the relevant tag value:

type Page struct {
    PageID string                 `bson:"pageId" json:"pageId"`
    Meta   map[string]interface{} `bson:"meta,omitempty" json:"meta,omitempty"`
}

Hiding an internal field from JSON

A field can have different behavior for each encoder:

type Page struct {
    PageID       string `bson:"pageId" json:"pageId"`
    InternalNotes string `bson:"internalNotes" json:"-"`
}

json:"-" means the field is never included in JSON output, while the BSON library can still store it.

For larger applications, developers often use separate database and API structs when their shapes or security rules differ. This prevents internal fields from being accidentally returned in an HTTP response.

Common Mistakes

Separating tag keys with a comma

This is invalid struct-tag syntax for two keys:

type Page struct {
    PageID string `bson:"pageId",json:"pageId"`
}

Use a space between tag entries:

type Page struct {
    PageID string `bson:"pageId" json:"pageId"`
}

Giving two fields the same JSON name by accident

The attempted Meta tag used json:"pageId". That creates duplicate JSON keys or ambiguous behavior.

// Incorrect
type Page struct {
    PageID string                 `json:"pageId"`
    Meta   map[string]interface{} `json:"pageId"`
}

Use json:"meta" for Meta.

Using unexported fields

The standard JSON package ignores unexported fields:

type Page struct {
    pageID string `json:"pageId"` // ignored by encoding/json
}

Export the field by starting its Go name with an uppercase letter:

type Page struct {
    PageID string `json:"pageId"`
}

Putting omitempty outside the tag value

Incorrect:

PageID string `json:"pageId",omitempty`

Correct:

PageID string `json:"pageId,omitempty"`

Expecting tags to rename the Go field

Tags only affect encoders and decoders that understand them. In Go code, you still access the field as page.PageID, not page.pageId.

Comparisons

SituationSyntaxMeaning
One JSON name`json:"pageId"`JSON uses pageId.
BSON and JSON names`bson:"pageId" json:"pageId"`Each encoder uses its own tag.
JSON field optional`json:"pageId,omitempty"`Omit the field when it has an empty value.
Exclude from JSON`json:"-"`Never serialize or deserialize this field as JSON.
Different external names`bson:"page_id" json:"pageId"`MongoDB and JSON can use different conventions.

A comma has two different roles:

LocationRole
Between tag entriesDo not use a comma; use a space.
Inside one tag valueUse a comma for options, such as json:"meta,omitempty".
Take Quiz

Cheat Sheet

// Multiple tag keys: separate with spaces.
type Page struct {
    PageID string                 `bson:"pageId" json:"pageId"`
    Meta   map[string]interface{} `bson:"meta" json:"meta"`
}
// Options belong after the external name, inside one tag value.
type Page struct {
    Meta map[string]interface{} `bson:"meta,omitempty" json:"meta,omitempty"`
}
// Exclude from one format.
type Page struct {
    Secret string `bson:"secret" json:"-"`
}

Rules:

  • Wrap the entire tag in backticks.
  • Separate tag keys with spaces.
  • Use commas only for options such as omitempty.
  • Use exported Go fields for encoding/json.
  • Ensure each field has the intended JSON and BSON name.
  • Tags affect serialization libraries, not how fields are accessed in Go code.

FAQ

Can a Go struct field have multiple tags?

Yes. Put multiple key:"value" entries inside one backtick-delimited struct tag, separated by spaces.

Why does encoding/json output PageId by default?

Without a json tag, encoding/json uses the exported Go field name as the JSON key.

Should I use PageId or PageID in Go?

Both compile, but PageID follows the common Go convention for initialisms. The JSON or BSON tag can still be pageId.

Can BSON and JSON use different field names?

Yes:

PageID string `bson:"page_id" json:"pageId"`

What does omitempty do?

It omits a field during encoding when the value is empty, such as "", 0, false, nil, or an empty slice/map.

Does json:"-" prevent MongoDB from storing the field?

No. It only affects JSON. Add bson:"-" too if you also want the BSON encoder to ignore it.

Are struct tags checked by the Go compiler?

The compiler accepts tags as strings. Whether they are meaningful depends on the package reading them. Tools such as go vet can detect malformed tags.

Take Quiz

Mini Project

Description

Build a small Go program that represents a page retrieved from MongoDB and serializes it as an API-friendly JSON response. It demonstrates using BSON and JSON tags on the same struct, including an optional field that is not exposed publicly.

Goal

Create a Page value and print JSON with lower camel-case field names while retaining MongoDB BSON mappings.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more