How Telego Handles JSON Serialization: Build Tags and Runtime Configuration for go-json and Sonic

Telego abstracts JSON marshaling and unmarshaling behind configurable function pointers in internal/json, enabling compile-time selection via build tags (stdjson, sonic, go-json) and runtime injection through SetJSONMarshal and SetJSONUnmarshal.

Telego, the feature-rich Go library for the Telegram Bot API maintained at mymmrac/telego, implements a pluggable JSON architecture that decouples core bot logic from specific serialization engines. This design allows developers to optimize Telego JSON serialization performance by selecting high-performance libraries like go-json or sonic, or to inject mock implementations for unit testing without modifying application code.

Core Abstraction in internal/json

The JSON layer centers on two package-level function variables declared inside the telego/internal/json package. All request and response processing throughout the library funnels through these variables, ensuring zero impact on business logic when swapping engines.

The Marshal and Unmarshal Variables

According to the source code in internal/json/common.go, the package exposes:

var (
    Marshal   func(v any) ([]byte, error)
    Unmarshal func(data []byte, v any) error
)

Every Telegram API object serialized by Telego invokes json.Marshal and json.Unmarshal through these function pointers. At initialization, one of three build-tag-controlled files assigns an implementation to these variables.

Build Tag Controlled Implementations

The library uses Go build constraints to select the JSON engine at compile time. Only one implementation file is compiled per binary:

Build Tag Source File JSON Engine
stdjson (default) internal/json/lib.std.go Standard library encoding/json
sonic internal/json/lib.sonic.go Bytedance high-performance sonic
(no tag) for go-json internal/json/lib.gojson.go github.com/grbit/go-json

If no build tag is specified, the default stdjson implementation from lib.std.go is selected. To use go-json, ensure no sonic tag is set and the go-json file compiles as the fallback.

Compile-Time Configuration

Selecting the JSON engine requires no code changes—only a flag during the build process.

Default Standard Library

Building without tags automatically uses encoding/json:

go build -o telego-bot .

This corresponds to the implementation in internal/json/lib.std.go, which assigns json.Marshal and json.Unmarshal from the standard library to the package variables.

High-Performance Sonic

To enable Bytedance’s sonic parser (optimized for JIT and large payloads), add the build tag:

go build -tags=sonic -o telego-bot .

The file internal/json/lib.sonic.go will be compiled, linking sonic.Marshal and sonic.Unmarshal to the internal variables.

Fallback go-json Implementation

The file internal/json/lib.gojson.go provides the go-json implementation. This compiles automatically when the sonic tag is absent, using github.com/grbit/go-json for faster parsing than the standard library without CGO dependencies.

Runtime Configuration API

Even after compilation, Telego exposes public setters in json.go to replace the JSON engine dynamically. This is useful for integrating libraries not covered by build tags or for dependency injection during tests.

SetJSONMarshal and SetJSONUnmarshal

The functions defined in json.go allow runtime replacement:

func SetJSONMarshal(marshal func(v any) ([]byte, error))
func SetJSONUnmarshal(unmarshal func(data []byte, v any) error)

Both functions panic if passed nil, and they are not safe for concurrent use after the bot has started because they modify global variables without synchronization. Configure these before calling telego.NewBot.

Example: Injecting json-iterator at Runtime

package main

import (
    "log"

    "github.com/mymmrac/telego"
    jsoniter "github.com/json-iterator/go"
)

func main() {
    // Replace with a custom JSON library
    telego.SetJSONMarshal(jsoniter.Marshal)
    telego.SetJSONUnmarshal(jsoniter.Unmarshal)

    bot, err := telego.NewBot("YOUR_BOT_TOKEN")
    if err != nil {
        log.Fatalf("Failed to create bot: %v", err)
    }
    defer bot.Stop()

    // All subsequent API calls use json-iterator for serialization
}

Practical Implementation Examples

Mocking JSON for Unit Tests

The abstraction enables test doubles that capture or mutate payloads without network calls:

func TestSendMessagePayload(t *testing.T) {
    var captured []byte
    
    // Inject mock marshal that captures the serialized data
    telego.SetJSONMarshal(func(v any) ([]byte, error) {
        b, err := json.Marshal(v)
        captured = b
        return b, err
    })
    
    bot, _ := telego.NewBot("fake-token")
    // ... invoke bot.SendMessage(...)
    
    // Assert that `captured` contains expected JSON structure
    if !bytes.Contains(captured, []byte(`"chat_id":12345`)) {
        t.Error("Missing expected chat_id in payload")
    }
}

Switching to go-json in Production

To deploy a binary using the go-json engine (fallback when sonic is not tagged), verify your build command excludes the sonic tag:


# Explicitly ensure sonic is disabled, allowing lib.gojson.go to compile

go build -tags=!sonic -o telego-bot .

This links the github.com/grbit/go-json implementation, offering better performance than encoding/json for high-throughput bots.

Summary

  • Telego JSON serialization is handled through function variables in internal/json, not direct standard library calls.
  • Build tags (stdjson, sonic) control which file (lib.std.go, lib.sonic.go, or lib.gojson.go) initializes the marshal functions at compile time.
  • Runtime flexibility is provided by SetJSONMarshal and SetJSONUnmarshal in json.go, though these must be set before bot initialization and are not thread-safe.
  • Testability is improved because mock JSON functions can be injected to capture or modify API payloads without altering core library code.

Frequently Asked Questions

How do I configure Telego to use go-json instead of the standard library?

Ensure you are not using the sonic build tag. The file internal/json/lib.gojson.go automatically compiles as the fallback, assigning github.com/grbit/go-json to the internal marshal variables. Build with plain go build or explicitly exclude sonic: go build -tags=!sonic.

Can I switch JSON libraries after the bot has started?

No. While SetJSONMarshal and SetJSONUnmarshal allow runtime configuration, they modify global variables without synchronization. Changing them after calling telego.NewBot creates a data race. Always configure JSON handlers during application initialization, before creating the bot instance.

What is the performance difference between stdjson, go-json, and sonic in Telego?

According to the implementation layout, sonic (enabled via -tags=sonic) provides the highest performance via JIT compilation but requires CGO. go-json (the fallback in lib.gojson.go) offers a significant speedup over stdjson without CGO, making it ideal for pure-Go deployments. stdjson remains the default for maximum compatibility.

Where are the JSON configuration functions defined?

The public API SetJSONMarshal and SetJSONUnmarshal are defined in the root json.go file. The internal variables they modify are declared in internal/json/common.go, while the engine-specific implementations reside in internal/json/lib.std.go, internal/json/lib.sonic.go, and internal/json/lib.gojson.go.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →