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

> Explore how Telego manages JSON serialization using go-json build tags and runtime configuration. Optimize your Go projects with flexible JSON handling.

- Repository: [Artem Yadelskyi/telego](https://github.com/mymmrac/telego)
- Tags: internals
- Published: 2026-03-07

---

**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`](https://github.com/mymmrac/telego/blob/main/internal/json/common.go), the package exposes:

```go
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`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.std.go) | Standard library `encoding/json` |
| `sonic` | [`internal/json/lib.sonic.go`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.sonic.go) | Bytedance high-performance `sonic` |
| *(no tag)* for go-json | [`internal/json/lib.gojson.go`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.gojson.go) | `github.com/grbit/go-json` |

If no build tag is specified, the default `stdjson` implementation from [`lib.std.go`](https://github.com/mymmrac/telego/blob/main/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`:

```bash
go build -o telego-bot .

```

This corresponds to the implementation in [`internal/json/lib.std.go`](https://github.com/mymmrac/telego/blob/main/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:

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

```

The file [`internal/json/lib.sonic.go`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/json.go) allow runtime replacement:

```go
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

```go
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:

```go
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:

```bash

# 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`](https://github.com/mymmrac/telego/blob/main/lib.std.go), [`lib.sonic.go`](https://github.com/mymmrac/telego/blob/main/lib.sonic.go), or [`lib.gojson.go`](https://github.com/mymmrac/telego/blob/main/lib.gojson.go)) initializes the marshal functions at compile time.
- **Runtime flexibility** is provided by `SetJSONMarshal` and `SetJSONUnmarshal` in [`json.go`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/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`](https://github.com/mymmrac/telego/blob/main/json.go) file. The internal variables they modify are declared in [`internal/json/common.go`](https://github.com/mymmrac/telego/blob/main/internal/json/common.go), while the engine-specific implementations reside in [`internal/json/lib.std.go`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.std.go), [`internal/json/lib.sonic.go`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.sonic.go), and [`internal/json/lib.gojson.go`](https://github.com/mymmrac/telego/blob/main/internal/json/lib.gojson.go).