kyaml 0.3.1

A YAML subset that hurts less

KYAML

A Crystal implementation of KYAML (Kubernetes YAML), a less ambiguous YAML subset as specified in KEP-5295. This shard enables Crystal applications to parse and emit KYAML, with comment preservation, type-safe de/serialization, and a streaming builder.

What is KYAML?

KYAML is a stricter subset of YAML designed to avoid some of its common pitfalls while still prioritizing ease of use:

  • Not whitespace-sensitive: Uses flow-style {} and [] for mappings and sequences instead of block-style indentation. Never get lost in indentation hell again!
  • Still allows comments: Unlike JSON, but like YAML. Because it is YAML.
  • Allows trailing commas: Easier editing and cleaner diffs.
  • Unambiguous strings: Always double-quotes value strings to avoid the dreaded "Norway bug" (NOfalse).
  • Unquoted keys: No need to quote keys unless they are ambiguous (e.g., no, true, null).

Every KYAML doc is a valid YAML doc, so existing YAML tooling can still consume it!

And now let's compare KYAML's features to our good ole friends YAML and JSON:

Feature YAML JSON KYAML
Comments Yes No Yes!
Trailing commas Yes No Yes!
Quoted keys required No Yes No!
Whitespace sensitive Yes No No!
String value quoting Optional Required Required
Flow-style brackets Optional Required Required
Mistakenly treats strings as booleans Yes No No!
Not-HCL Yes Yes Yes!
---
{
  apiVersion: "v1",
  kind: "Service",
  metadata: {
    name: "my-service",
    labels: {
      app: "myapp",
    },
  },
  spec: {
    ports: [{
      port: 80,
      targetPort: 9376,
    }],
  },
}

Installation

Add the dependency to your shard.yml:

dependencies:
  kyaml:
    github: lowkeyliesmyth/kyaml

Run shards install

Quick Start

Parsing

KYAML.parse returns a KYAML::Any you can traverse, directly mirroring YAML::Any. Unsupported Time and Bytes types are coerced into strings, and Setinto a mapping with null values.

require "kyaml"

doc = KYAML.parse(<<-KYAML)
  {
      name: "foo",
      port: 8080,
      tags: ["web", "api"]
  }
  KYAML

doc["name"].as_s # => "foo"
doc["port"].as_i # => 8080
doc["tags"][0].as_s # => "web"

Emitting

KYAML.emit renders any standard basic Crystal value (scalars, Array, Hash, KYAML::Any) as KYAML. #to_kyaml is the same on any basic value, and on any type including KYAML::Serializable:

{"name" => "foo", "ports" => [80, 443]}.to_kyaml
# {
#   name: "foo",
#   ports: [
#     80,
#     443,
#   ],
# }

Type-safe serialization

Include KYAML::Serializable for typed round-trips, mirroring stdlib's YAML::Serializable:

struct Service
  include KYAML::Serializable

  @[KYAML::Field(key: "apiVersion")]
  property api_version : String
  property name : String
  property port : Int32 = 80
end

svc = Service.from_kyaml(%({ apiVersion: "v1", name: "foo"}))

svc.api_version # => "v1"
svc.name # => "foo"
svc.to_kyaml # => {
             #      apiVersion: "v1",
             #      name: "foo",
             #      port: 80,
             #    }

Supported field annotations:

  • key:
  • ignore:
  • converter:
  • emit_null:
  • presence:

Type-level:

  • @[KYAML::Serializable::Options(emit_nulls: true)]

Mix in KYAML::Serializable::Strict to reject unknown keys or KYAML::Serializable::Unmapped to capture them in #kyaml_unmapped.

Polymorphic types use use_kyaml_discriminator.

See specs for more usage examples.

Comment Preservation

You know what's actually pretty nice about YAML? Comments, baby.

We got 'em, we track 'em, we keep 'em. parse_doc and emit_doc retain comments across a round-trip.

doc = KYAML.parse_doc(<<-KYAML)
# service definition
{
  name: "web", # public name
}
KYAML

KYAML.emit_doc(doc) # both comments are preserved in output. pinky-swear.

Streaming builder

Build a KYAML doc imperatively right in crystal without an intermediate object.

KYAML.build do |k|
  k.mapping do
    k.field("name", "foo")
    k.field("ports") do
      k.sequence do
        k.scalar(80)
        k.scalar(443)
      end
    end
  end
end

Multiple docs

KYAML.parse_all(input) do |doc|
  puts doc["kind"]
end

Strict-mode input validation

By default parsing runs in lenient mode so any valid YAML is accepted, including block-style. This lets you read existing YAML and convert it at emit time to KYAML.

Pass in strict: true to reject non-conforming KYAML constructs (eg block-style, anchors, aliases, explicit tags) with a KYAML::StrictError.

KYAML.parse(input, strict: true)

One rule is enforced across both strict and lenient modes though: any mapping key that is a non-scalar type will raise KYAML::NonStringKeyError.

Output

String output is ASCII safe. Printable ASCII is emitted literally, except for " and \, which are escaped. Common whitespace uses named escapes (\n, \r, \t). Everything else, both ASCII control characters and non-ASCII Unicode, is rendered as \uXXXX for the BMP and \UXXXXXXXX for ASTRAL PLANE!

KYAML.emit({"desc" => "line one\nline two\n  indented"})
# {
#   desc: "\
#     line one\n\
#     line two\n\
#     \u0020 indented",
# }

Development

# Run spec tests
task spec

# Run ameba linter
task lint

# Format code
crystal tool format

# Run a specific test file
crystal spec spec/any_spec.cr

Contributing

  1. Fork it (https://github.com/lowkeyliesmyth/kyaml/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request with a description following Conventional Commits.

License

MIT License - see LICENSE for details.

References

Repository

kyaml

Owner
Statistic
  • 0
  • 0
  • 0
  • 1
  • 1
  • 11 days ago
  • March 19, 2026
License

MIT License

Links
Synced at

Thu, 13 Aug 2026 07:01:30 GMT

Languages