---
title: How to Handle Dynamic or Unknown JSON in Go
summary: Use map[string]any or json.RawMessage to parse JSON with unknown or changing structures in Go.
lang: en
slug: how-to-handle-dynamic-or-unknown-json-in-go
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/how-to-handle-dynamic-or-unknown-json-in-go/
---

# How to Handle Dynamic or Unknown JSON in Go



## When the JSON shape changes

You are building a webhook handler for a third-party service. The documentation says the payload varies by event type. Sometimes you get a user signup with an email and name. Sometimes you get a payment failure with a transaction ID and error code. You could write a massive struct with every possible field, but that feels brittle. New fields appear, optional fields become required, and the schema drifts. Or you receive a JSON blob from an API and you only care about one deeply nested value, but the path depends on runtime data. Go's type system loves structure, but the web is messy. You need a way to accept JSON without knowing its shape ahead of time.

## The two standard tools

Go provides two standard approaches for dynamic JSON. The first is `map[string]any`. This unmarshals the JSON into a generic map where keys are strings and values can be anything. The second is `json.RawMessage`. This is a type alias for `[]byte`. It tells the decoder to skip parsing a field and just save the raw JSON bytes. You parse it later, or not at all.

Think of `map[string]any` like a mesh cargo crate. You throw everything in, and you sort through it later by feeling around for what you need. Think of `json.RawMessage` like a sealed envelope. You know it contains data, but you leave it unopened until you have a reason to look inside. Both approaches trade compile-time guarantees for runtime flexibility. Pick the one that matches your parsing strategy.

## Unmarshaling into a map

Here is the simplest way to accept unknown JSON and extract a value safely.

```go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// Raw JSON string with mixed types
	jsonStr := `{"name": "Alice", "age": 30, "active": true}`

	// Map keys are always strings. Values can be any type.
	var data map[string]any
	if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Type assertion checks the dynamic type at runtime.
	if name, ok := data["name"].(string); ok {
		fmt.Println("Name:", name)
	}

	// Numbers come back as float64 by default.
	if age, ok := data["age"].(float64); ok {
		fmt.Println("Age:", int(age))
	}
}
```

Maps are flexible. Type assertions are runtime checks. Use comma-ok or risk a panic.

## How the decoder decides types

When you unmarshal into `map[string]any`, the `encoding/json` package inspects the JSON tokens and builds a map. It infers the Go type for each value. Strings map to `string`. Booleans map to `bool`. Arrays become `[]any`. Objects become nested `map[string]any`.

Numbers always map to `float64`. This is a common surprise. If the JSON contains `42`, the map holds a `float64` with value `42.0`. If you try to assert `int`, the assertion fails. The compiler will not stop you, but the runtime will reject the assertion. You must assert `float64` and convert to `int` if needed. The decoder makes this choice because JSON does not distinguish between integers and floating-point numbers. A `float64` can represent any valid JSON number without loss of precision for typical payloads.

Null values in JSON become `nil` in Go. A map entry for a null value holds a `nil` interface. Accessing it requires checking for nil before asserting. If you assert a type on a nil interface, the assertion returns false.

Go 1.18 introduced `any` as an alias for `interface{}`. Modern code uses `any`. Older codebases might still use `interface{}`. They are identical. The community convention is to use `any` in new code. It reads cleaner and signals that the type is intentionally open.

The decoder also respects struct tags when you unmarshal into a struct, but maps ignore tags entirely. Map keys are always the exact strings from the JSON. Case sensitivity matters. `Name` and `name` are different keys.

Maps trade safety for flexibility. Verify every assertion.

## Lazy parsing with RawMessage

Real code often needs partial parsing. You receive a large JSON document but only care about a few fields. Or the structure of a nested object depends on a top-level discriminator. `json.RawMessage` solves this. You define a struct with a field of type `json.RawMessage`. The decoder fills that field with the raw JSON bytes. It does not parse the content. You can inspect the discriminator field, then call `json.Unmarshal` on the raw bytes to parse into the correct type.

Here is how to parse a discriminator and lazily extract the payload.

```go
package main

import (
	"encoding/json"
	"fmt"
)

// Event captures the top-level type and defers payload parsing.
type Event struct {
	Type    string          `json:"type"`
	Payload json.RawMessage `json:"payload"`
}

func process(jsonData []byte) {
	var event Event
	// Top-level unmarshal keeps Payload as raw bytes.
	if err := json.Unmarshal(jsonData, &event); err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Parse Payload only if the type matches.
	if event.Type == "signup" {
		var email string
		// Extract just the email from the raw payload.
		if err := json.Unmarshal(event.Payload, &email); err != nil {
			fmt.Println("Inner error:", err)
			return
		}
		fmt.Println("Email:", email)
	}
}
```

`json.RawMessage` is literally `type RawMessage []byte`. You can convert it to a string with `string(rawMsg)` to log the raw JSON, or pass it directly to another unmarshal call. This is useful for debugging or forwarding payloads to a downstream service without touching the data.

RawMessage is lazy. Parse only what you need.

## Pitfalls and runtime failures

Dynamic JSON moves errors from compile time to runtime. The compiler cannot check types. If you expect a string and get a number, the type assertion fails. Without the comma-ok idiom, your program panics with `interface conversion: interface {} is float64, not string`. Always use the comma-ok form `val, ok := data["key"].(string)` with dynamic data. The comma-ok pattern returns a boolean that tells you whether the assertion succeeded. It prevents crashes and lets you handle missing or mismatched fields gracefully.

If you unmarshal into the wrong type, the decoder rejects the input. Passing a string variable to `json.Unmarshal` when the JSON is an object produces `json: cannot unmarshal object into Go value of type string`. The error message tells you exactly what went wrong. Check the error before proceeding.

Performance is another concern. Unmarshaling into `map[string]any` allocates a new map and interface for every node. It is significantly slower than unmarshaling into a struct. For a 10MB JSON file, the memory usage can spike. If you are processing high-throughput data, `map[string]any` might trigger GC pressure. `json.RawMessage` avoids this by keeping data as a byte slice. You can also use `json.Decoder` with a buffer to stream data instead of loading everything into memory at once. Streaming decoders read tokens one at a time and skip branches you do not need.

The community convention for error handling applies here too. `if err != nil { return err }` is verbose by design. The boilerplate makes the unhappy path visible. Do not swallow errors from `json.Unmarshal`. Log them or return them. Silent failures in JSON parsing turn into corrupted state downstream.

Dynamic JSON moves errors from compile time to runtime. Test the edge cases.

## Decision: struct, map, or raw bytes

Use a typed struct when the JSON schema is stable and known. Structs give you compile-time safety, faster unmarshaling, and clear documentation.

Use `map[string]any` when the keys vary or the structure changes per request. This works for config files, generic webhooks, or debugging tools that need to inspect arbitrary JSON.

Use `json.RawMessage` when you need to parse a subset of the JSON or defer parsing based on a discriminator field. This avoids parsing data you will never use and handles polymorphic payloads efficiently.

Use a third-party library like `github.com/tidwall/gjson` when you need to extract values from deep paths without defining any types. GJSON uses a path syntax and is optimized for read-only access to large documents.

Structs for safety. Maps for chaos. RawMessage for control.

## Where to go next

- [Fix: "json: unsupported type" in Go](/en/fix-json-unsupported-type-in-go/)
- [Fix: "json: cannot unmarshal X into Go value of type Y"](/en/fix-json-cannot-unmarshal-x-into-go-value-of-type-y/)
- [How to Use MessagePack in Go](/en/how-to-use-messagepack-in-go/)