---
title: "The YAML Norway problem"
url: https://recursiveai.org/oneyaml/articles/yaml-norway-problem/
description: "The YAML Norway problem explained: why country: NO reads as false, which Go, Rust and C# YAML libraries still do it, and how to read and write YAML safely."
date: 2026-10-26
author: oneyaml team
---

# The YAML Norway problem

The YAML Norway problem explained: why country: NO reads as false, which Go, Rust and C# YAML libraries still do it, and how to read and write YAML safely.

October 26, 2026 · oneyaml team

The YAML Norway problem is what happens when a parser reads `country: NO` as the boolean `false` instead of the country code for Norway. It comes from YAML 1.1, which treats `yes`, `no`, `on`, `off`, `y` and `n` as booleans and a leading zero as octal. YAML 1.2 fixed this in 2009: its Core schema has only `true` and `false` as booleans. But many libraries still read some 1.1 forms by default, so the fix in your code depends on which library you use. Below, the same three lines are read by nine widely used Go, Rust and C# libraries and by oneyaml, followed by the rules that keep a file safe whichever reader ends up with it.

## Why NO becomes false

YAML guesses the type of every unquoted (plain) scalar. Which guesses it makes depends on the schema. The [YAML 1.1 boolean type](https://yaml.org/type/bool.html) resolves this pattern to a boolean:

*yaml.org/type/bool.html · YAML 1.1*

```
y|Y|yes|Yes|YES|n|N|no|No|NO
|true|True|TRUE|false|False|FALSE
|on|On|ON|off|Off|OFF
```

So `NO`, `n` and `off` are all `false`. The [YAML 1.1 integer type](https://yaml.org/type/int.html) adds more surprises: a leading `0` means octal, `_` is ignored inside numbers, and `:` means base 60, so `22:30` is the integer 1350.

The [YAML 1.2.2 Core schema](https://yaml.org/spec/1.2.2/#1032-tag-resolution) resolves far fewer forms. A boolean is `true`, `True`, `TRUE`, `false`, `False` or `FALSE`. An integer is `[-+]? [0-9]+` in base 10, `0o` octal or `0x` hex. Anything that matches no rule is a string.

## YAML boolean values in 1.1 and 1.2

| Plain scalar | YAML 1.1 | YAML 1.2 Core schema |
|---|---|---|
| `NO`, `no`, `n`, `off` | boolean `false` | string |
| `yes`, `y`, `on` | boolean `true` | string |
| `true`, `False` | boolean | boolean |
| `0755` | octal, the integer 493 | the integer 755 |
| `0o755` | string | octal, the integer 493 |
| `1_000` | the integer 1000 | string |
| `22:30` | base 60, the integer 1350 | string |

One correction to a common claim: under YAML 1.2, `0755` is not a string. It matches the base-10 integer rule, so it is the number 755. The only way to get the text `0755` in every version is to quote it.

## What Go YAML libraries do with it

This program reads `country: no`, `enabled: yes` and `mode: 0755` into a `map[string]any` with four Go libraries:

*main.go · Go 1.26*

```
package main
 
import (
    "fmt"
 
    goccy "github.com/goccy/go-yaml"
    yamlv3 "go.yaml.in/yaml/v3"
    oneyaml "oneyaml.dev/go"
    k8syaml "sigs.k8s.io/yaml"
)
 
const src = "country: no\nenabled: yes\nmode: 0755\n"
 
func main() {
    decoders := []struct {
        name      string
        unmarshal func([]byte, any) error
    }{
        {"go.yaml.in/yaml/v3", yamlv3.Unmarshal},
        {"sigs.k8s.io/yaml", func(b []byte, v any) error { return k8syaml.Unmarshal(b, v) }},
        {"goccy/go-yaml", goccy.Unmarshal},
        {"oneyaml.dev/go", func(b []byte, v any) error { return oneyaml.Unmarshal(b, v) }},
    }
    for _, d := range decoders {
        var m map[string]any
        if err := d.unmarshal([]byte(src), &m); err != nil {
            fmt.Printf("%-20s error: %v\n", d.name, err)
            continue
        }
        fmt.Printf("%-20s country=%#v enabled=%#v mode=%v\n", d.name, m["country"], m["enabled"], m["mode"])
    }
}
```

*Output*

```
go.yaml.in/yaml/v3   country="no" enabled="yes" mode=493
sigs.k8s.io/yaml     country=false enabled=true mode=493
goccy/go-yaml        country="no" enabled="yes" mode=493
oneyaml.dev/go       country="no" enabled="yes" mode=755
```

Untyped, only sigs.k8s.io/yaml has the Norway problem, because it converts YAML to JSON with YAML 1.1 rules before `encoding/json` sees it. Typed fields tell a different story:

*main.go · Go 1.26*

```
package main
 
import (
    "fmt"
    "strings"
 
    goccy "github.com/goccy/go-yaml"
    yamlv3 "go.yaml.in/yaml/v3"
    oneyaml "oneyaml.dev/go"
    k8syaml "sigs.k8s.io/yaml"
)
 
type Settings struct {
    Country string `yaml:"country" json:"country"`
    Enabled bool   `yaml:"enabled" json:"enabled"`
    Mode    int    `yaml:"mode" json:"mode"`
}
 
func main() {
    decoders := []struct {
        name      string
        unmarshal func([]byte, any) error
    }{
        {"go.yaml.in/yaml/v3", yamlv3.Unmarshal},
        {"sigs.k8s.io/yaml", func(b []byte, v any) error { return k8syaml.Unmarshal(b, v) }},
        {"goccy/go-yaml", goccy.Unmarshal},
        {"oneyaml.dev/go", func(b []byte, v any) error { return oneyaml.Unmarshal(b, v) }},
    }
    for _, d := range decoders {
        var cells []string
        for _, src := range []string{"country: no", "enabled: yes", "mode: 0755"} {
            var s Settings
            if err := d.unmarshal([]byte(src), &s); err != nil {
                cells = append(cells, "error")
                continue
            }
            switch {
            case strings.HasPrefix(src, "country"):
                cells = append(cells, fmt.Sprintf("%q", s.Country))
            case strings.HasPrefix(src, "enabled"):
                cells = append(cells, fmt.Sprint(s.Enabled))
            default:
                cells = append(cells, fmt.Sprint(s.Mode))
            }
        }
        fmt.Printf("%-19s Country=%-8s Enabled=%-6s Mode=%s\n", d.name, cells[0], cells[1], cells[2])
    }
}
```

*Output*

```
go.yaml.in/yaml/v3  Country="no"     Enabled=true   Mode=493
sigs.k8s.io/yaml    Country="false"  Enabled=true   Mode=493
goccy/go-yaml       Country="no"     Enabled=error  Mode=493
oneyaml.dev/go      Country="no"     Enabled=error  Mode=755
```

- **sigs.k8s.io/yaml** v1.6.0 stores the text `"false"` in a `string` field: the country code is lost even with a typed struct.
- **go.yaml.in/yaml/v3** v3.0.5 reads `yes` as `true` into a `bool` field, as its [README](https://github.com/go-yaml/yaml/blob/v3/README.md) says (YAML 1.1 booleans “are supported as long as they are being decoded into a typed bool value”), and `0755` as octal 493.
- **goccy/go-yaml** v1.19.2 refuses `yes` for a `bool` but reads `0755` as 493.
- **oneyaml.dev/go** v1.0.0 follows the Core schema in both cases: `yes` into a `bool` is an error with its line and column (`line 1, column 10: /enabled: cannot unmarshal !!str `yes` into bool`), and `0755` is 755.

## Rust: the serde_yaml family and serde-saphyr

The same three lines, read into typed fields:

*main.rs · Rust 2024*

```
use serde::Deserialize;
 
#[derive(Deserialize)]
struct Settings {
    country: Option<String>,
    enabled: Option<bool>,
    mode: Option<i64>,
}
 
fn row(name: &str, read: fn(&str) -> Result<Settings, String>) {
    let cell = |src: &str| match read(src) {
        Ok(s) => s.country.map(|c| format!("{c:?}"))
            .or(s.enabled.map(|b| b.to_string()))
            .or(s.mode.map(|m| m.to_string()))
            .unwrap_or_default(),
        Err(_) => "error".to_string(),
    };
    println!("{name:<22} country={:<8} enabled={:<6} mode={}",
        cell("country: no"), cell("enabled: yes"), cell("mode: 0755"));
}
 
fn main() {
    row("serde_yaml_ng 0.10.0", |s| serde_yaml_ng::from_str(s).map_err(|e| e.to_string()));
    row("serde_norway 0.9.42", |s| serde_norway::from_str(s).map_err(|e| e.to_string()));
    row("yaml_serde 0.10.7", |s| yaml_serde::from_str(s).map_err(|e| e.to_string()));
    row("serde-saphyr 2.0.0", |s| serde_saphyr::from_str(s).map_err(|e| e.to_string()));
    row("oneyaml 1.2.0", |s| oneyaml::from_str(s).map_err(|e| e.to_string()));
}
```

*Output*

```
serde_yaml_ng 0.10.0   country="no"     enabled=error  mode=error
serde_norway 0.9.42    country="no"     enabled=error  mode=error
yaml_serde 0.10.7      country="no"     enabled=error  mode=error
serde-saphyr 2.0.0     country="no"     enabled=true   mode=error
oneyaml 1.2.0          country="no"     enabled=error  mode=755
```

The serde_yaml forks (serde_yaml_ng, serde_norway and yaml_serde) don’t read `yes` as a boolean. They read `0755` as the string `"0755"`, so an integer field refuses it. serde-saphyr makes the Rust type the schema: by default it accepts YAML 1.1 forms such as `no` and `off` when the target is a `bool`, and its `strict_booleans` option turns that off ([README](https://github.com/bourumir-wyngs/serde-saphyr#booleans)). oneyaml reads the Core schema, so `0755` is 755 and `yes` into a `bool` is an error whose message names the cause: `YAML 1.2 reads `yes` as a string (its booleans are `true` and `false`)`.

## C#: YamlDotNet, SharpYaml and OneYaml

*Program.cs · .NET 10*

```
using System.Text.Json;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
 
var ydn = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
var sharpOptions = new SharpYaml.YamlSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
 
var readers = new (string Name, Func<string, Settings> Typed, Func<string, object?> Untyped)[]
{
    ("YamlDotNet 18.1.0", ydn.Deserialize<Settings>, yaml => ydn.Deserialize<object>(yaml)),
    ("SharpYaml 3.16.0", yaml => SharpYaml.YamlSerializer.Deserialize<Settings>(yaml, sharpOptions)!,
        yaml => SharpYaml.YamlSerializer.Deserialize<object>(yaml)),
    ("OneYaml 1.2.1", yaml => OneYaml.YamlSerializer.Deserialize<Settings>(yaml)!,
        yaml => OneYaml.YamlSerializer.Deserialize<object>(yaml)),
};
 
foreach (var (name, typed, untyped) in readers)
{
    Console.WriteLine($"{name,-18} typed:   Country={Show(() => typed("country: no").Country),-7} " +
        $"Enabled={Show(() => typed("enabled: yes").Enabled),-6} Mode={Show(() => typed("mode: 0755").Mode)}");
    Console.WriteLine($"{"",-18} untyped: country={Show(() => Value(untyped("country: no"), "country")),-7} " +
        $"enabled={Show(() => Value(untyped("enabled: yes"), "enabled")),-6} mode={Show(() => Value(untyped("mode: 0755"), "mode"))}");
}
 
static object? Value(object? root, string key) => root switch
{
    IDictionary<object, object?> d => d[key],
    IDictionary<string, object?> d => d[key],
    _ => throw new InvalidOperationException(root?.GetType().Name),
};
 
static string Show<T>(Func<T> get)
{
    try
    {
        object? v = get();
        return v switch { string s => $"\"{s}\"", bool b => b ? "true" : "false", _ => $"{v}" };
    }
    catch (Exception) { return "error"; }
}
 
class Settings
{
    public string? Country { get; set; }
    public bool Enabled { get; set; }
    public int Mode { get; set; }
}
```

*Output*

```
YamlDotNet 18.1.0  typed:   Country="no"    Enabled=true   Mode=493
                   untyped: country="no"    enabled="yes"  mode="0755"
SharpYaml 3.16.0   typed:   Country="no"    Enabled=error  Mode=755
                   untyped: country="no"    enabled="yes"  mode=755
OneYaml 1.2.1      typed:   Country="no"    Enabled=error  Mode=755
                   untyped: country="no"    enabled="yes"  mode=755
```

YamlDotNet 18.1.0 reads `yes` as `true` and `0755` as 493 into typed members. Its [README](https://github.com/aaubry/YamlDotNet#readme) lists YAML 1.2 support for the parser and only 1.1 for the emitter. Untyped, it returns every scalar as a string. SharpYaml 3.16.0 and OneYaml 1.2.1 read the YAML 1.2 Core schema both ways.

## How to avoid the Norway problem

### Quote strings that look like something else

This is the only fix that works in every reader. Quote anything that is an identifier, even when it looks like a number or a word: country and language codes, versions, ZIP codes, file modes meant as text, SHAs, phone numbers. `country: "NO"` and `mode: "0755"` mean the same thing under YAML 1.1 and 1.2.

### Read with a YAML 1.2 Core schema parser

A typed struct alone doesn’t protect you, as the sigs.k8s.io/yaml and yaml.v3 rows show. Pick a library that reads the Core schema by default, and you get either the right value or an error that points at the line.

### Write YAML that every reader agrees on

Files you write today will be read by tools on YAML 1.1, so the writer should quote the strings a 1.1 reader would take for something else. oneyaml does this when it serializes, and a `Document` edit does it when you ask for it with `YAML11Strings()`:

*main.go · oneyaml.dev/go v1.0.0*

```
package main
 
import (
    "fmt"
    "log"
 
    oneyaml "oneyaml.dev/go"
)
 
type Site struct {
    Country string `yaml:"country"`
    Mode    string `yaml:"mode"`
    Debug   string `yaml:"debug"`
}
 
func main() {
    out, err := oneyaml.Marshal(Site{Country: "no", Mode: "0755", Debug: "on"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(string(out))
 
    src := []byte("country: se  # ISO 3166 code\n")
    for _, opts := range [][]oneyaml.Option{nil, {oneyaml.YAML11Strings()}} {
        doc, err := oneyaml.Parse(src, opts...)
        if err != nil {
            log.Fatal(err)
        }
        if err := doc.Set("/country", "no"); err != nil {
            log.Fatal(err)
        }
        fmt.Print(doc)
    }
}
```

*Output*

```
country: 'no'
mode: '0755'
debug: 'on'
country: no  # ISO 3166 code
country: 'no'  # ISO 3166 code
```

By default an edit judges plainness by the Core schema, so `no` is written plain, which oneyaml reads back as the string you set. With `YAML11Strings()`, it is quoted, which is what you want when Kubernetes tooling built on sigs.k8s.io/yaml or yaml.v2 also reads the file ([editing](https://recursiveai.org/oneyaml/go/docs/editing/)). The Rust `to_string` and C# `YamlSerializer.Serialize` quote such strings too: C# writes `'yes'` and `'007'` so that “the output reads back as the same values under YAML 1.2 and YAML 1.1” ([serializer](https://recursiveai.org/oneyaml/csharp/docs/serializer/)).

### Read old YAML 1.1 files on purpose

Sometimes a file was written for a 1.1 reader and `enabled: yes` really means `true`. Say so in code rather than changing what every file means. In Rust, use the `Compat::yaml_1_1()` preset, or let a file that starts with `%YAML 1.1` choose for itself:

*main.rs · oneyaml 1.2.0*

```
use oneyaml::{Compat, Settings};
use serde::Deserialize;
 
#[derive(Deserialize)]
struct Legacy {
    enabled: bool,
    mode: u32,
}
 
fn main() {
    let src = "enabled: yes\nmode: 0755\n";
 
    // YAML 1.2 by default: `yes` is a string, so a bool field refuses it.
    if let Err(err) = oneyaml::from_str::<Legacy>(src) {
        println!("default: {err}");
    }
 
    // Read a legacy YAML 1.1 file on purpose.
    let legacy: Legacy = oneyaml::from_str_with(src, &Compat::yaml_1_1()).unwrap();
    println!("yaml_1_1: enabled={} mode={}", legacy.enabled, legacy.mode);
 
    // Or let each file choose: `%YAML 1.1` switches that document to 1.1 rules.
    let declared = "%YAML 1.1\n---\nenabled: yes\nmode: 0755\n";
    let settings = Settings::new().honor_version_directive(true);
    let legacy: Legacy = oneyaml::from_str_with(declared, &settings).unwrap();
    println!("%YAML 1.1: enabled={} mode={}", legacy.enabled, legacy.mode);
}
```

*Output*

```
default: invalid type: string "yes", expected a boolean; YAML 1.2 reads `yes` as a string (its booleans are `true` and `false`) at enabled (1:10)
yaml_1_1: enabled=true mode=493
%YAML 1.1: enabled=true mode=493
```

In Go, the `compat/yamlv3` and `compat/k8syaml` packages read the YAML 1.1 forms that yaml.v3 and sigs.k8s.io/yaml read ([migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/)). In C#, the `OneYaml.Compat.YamlDotNet` and `OneYaml.Compat.SharpYaml` packages do the same for code moving from those libraries ([YamlDotNet compatibility](https://recursiveai.org/oneyaml/csharp/docs/compat-yamldotnet/)). The Rust cookbook has more on [YAML 1.1 booleans](https://recursiveai.org/oneyaml/rust/cookbook/yaml-1-1-booleans/).

## Where to go next

Choosing a library for one language: [Go YAML libraries](https://recursiveai.org/oneyaml/articles/golang-yaml/), [Rust YAML parsers](https://recursiveai.org/oneyaml/articles/rust-yaml-parser/) and [C# YAML parsers](https://recursiveai.org/oneyaml/articles/csharp-yaml-parser/). To change a value in a file without disturbing anything else, see [edit YAML without losing comments](https://recursiveai.org/oneyaml/articles/edit-yaml-keep-comments/). All three oneyaml libraries, and how to install them, are on the [oneyaml home page](https://recursiveai.org/oneyaml/). Library versions on this page are the latest published on 26 October 2026.
