# JSON and File I/O

> Source: https://learn-go.org/json-and-io/
> Part of Learn Go, free to read.

## JSON

```go
type User struct {
    ID        string    `json:"id"`
    Email     string    `json:"email"`
    Age       int       `json:"age,omitempty"`
    CreatedAt time.Time `json:"created_at"`
    password  string    // unexported — never serialised
}
```

```go
u := User{ID: "1", Email: "ada@example.com", CreatedAt: time.Now()}

data, err := json.Marshal(u)
// {"id":"1","email":"ada@example.com","created_at":"2026-09-05T..."}

var back User
err = json.Unmarshal(data, &back)     // note the & — it must be a pointer
```

`Age` is absent because of `omitempty`, which drops zero values. `password` is absent because it is lowercase.

:::danger Unexported fields disappear silently
```go
type Config struct {
    apiKey  string `json:"api_key"`    // lowercase: NEVER encoded or decoded
    Timeout int    `json:"timeout"`
}
```
`encoding/json` uses reflection and can only see exported fields. No error, no warning — the field is simply always its zero value after a round trip. This is the most common JSON bug in Go and it costs people an afternoon at least once.
:::

### Struct tag options

```go
`json:"name"`             // rename
`json:"name,omitempty"`   // omit if zero value
`json:"-"`                // never include
`json:",string"`          // encode a number as a JSON string
```

`json:"-"` is how you keep a secret out of a response while leaving the field exported for your own code:

```go
type User struct {
    Email        string `json:"email"`
    PasswordHash string `json:"-"`      // exported, but never serialised
}
```

### Decoding unknown shapes

```go
var raw map[string]any
json.Unmarshal(data, &raw)

if name, ok := raw["name"].(string); ok {
    fmt.Println(name)
}
```

Numbers decode into `float64` by default, which surprises people:

```go
n := raw["count"].(float64)     // not int
```

Prefer a struct whenever you know the shape — it is faster, type-safe and self-documenting. Reach for `map[string]any` only for genuinely dynamic data.

### Streaming

For anything large, or for HTTP, use the encoder and decoder rather than the whole-buffer functions:

```go
func handler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "invalid JSON", http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}
```

This avoids loading the entire body into memory. Two production details generated code omits:

```go
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)   // cap the request size
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()                       // reject unexpected fields
```

Without `MaxBytesReader` a client decides how much memory you allocate.

## Files

```go
data, err := os.ReadFile("config.json")     // whole file into memory
err = os.WriteFile("out.json", data, 0o644)
```

Simple and correct for small files. For large ones, stream:

```go
f, err := os.Open("large.log")
if err != nil {
    return fmt.Errorf("open: %w", err)
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
    line := scanner.Text()
    // ...
}
if err := scanner.Err(); err != nil {     // check this — Scan returns false on error too
    return fmt.Errorf("scan: %w", err)
}
```

`scanner.Err()` is easy to forget and is the difference between "finished the file" and "gave up halfway".

Writing with a buffer:

```go
f, err := os.Create("out.txt")
if err != nil { return err }
defer f.Close()

w := bufio.NewWriter(f)
defer w.Flush()          // without this, buffered data is lost
for _, line := range lines {
    fmt.Fprintln(w, line)
}
```

## io.Reader and io.Writer

The two interfaces that unify all I/O in Go, and the best example of why small interfaces work:

```go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
```

A file, a network connection, an HTTP body, a compressed stream, an in-memory buffer and standard input are all `io.Reader`s. So a function that takes one works with all of them:

```go
func countLines(r io.Reader) (int, error) {
    n := 0
    s := bufio.NewScanner(r)
    for s.Scan() { n++ }
    return n, s.Err()
}
```

```go
countLines(file)
countLines(resp.Body)
countLines(strings.NewReader("a\nb\nc"))     // trivially testable
countLines(os.Stdin)
```

That last line is the payoff: **accept `io.Reader` rather than a filename and your function becomes testable without touching the filesystem.** It is the single most useful API design habit in Go.

Useful helpers:

```go
io.Copy(dst, src)                    // stream one into the other
io.ReadAll(r)                        // whole thing into memory
io.LimitReader(r, 1<<20)             // cap it — use on untrusted input
io.MultiWriter(f, os.Stdout)         // write to both
```

## Exercise

```go
package main

import "fmt"

type Product struct {
	// Add tags so this encodes as:
	//   {"sku":"A1","name":"Widget","price_cents":499}
	// with an internal cost field that is NEVER serialised,
	// and a "discount" field omitted when zero.
	SKU  string
	Name string
	PriceCents int
	Discount int
	costCents int
}

func main() {
	// Encode a Product, print it, decode it back, print the result.
	fmt.Println("start")
}
```

## Common questions

### Why is my JSON field always empty?

Almost certainly because the struct field is lowercase. `encoding/json` uses reflection and cannot see unexported fields, so they are silently skipped in both directions. Capitalise the field and use a tag for the wire name.

### Why did my number become a float?

Decoding into `map[string]any` gives every JSON number as `float64`, since JSON has one numeric type. Decode into a struct with an `int` field, or use `json.Number` if you need the exact text.

### Should my function take a filename or an `io.Reader`?

An `io.Reader`. It makes the function work with files, network responses, buffers and stdin, and it makes tests trivial — `strings.NewReader` replaces a fixture file. Open the file in the caller.
