Parsing YAML with dynamic keys

Hi everyone, Crystal noob here :wave:

I’m creating my first Crystal app, a little CLI app that interfaces with a web application that provides a YAML-based API.

I’m trying serialize an API endpoint that returns YAML of the form:

---
item1:
  foo: 1
  barred_at: '2022-01-27T17:00:00Z'
  baz: true
item2:
  foo: 5
  barred_at: '2022-01-27T17:30:00Z'
  baz: false
item3:
  foo: 10
  barred_at: '2022-01-27T17:45:00Z'
  baz: true

using YAML::Serializable.

However as you can see, the top-level keys are dynamic and the examples in the docs all assume static keys. Any pointers on how to handle this use case would be much appreciated.

Thanks!

Hash#to_yaml should fit nicely here :slight_smile:

require "yaml"

record Item, foo : Int32, barred_at : Time, baz : Bool do
  include YAML::Serializable
end

data = {
  item1: Item.new(foo: 1, barred_at: Time.parse_iso8601("2022-01-27T17:00:00Z"), baz: true),
  item2: Item.new(foo: 5, barred_at: Time.parse_iso8601("2022-01-27T17:30:00Z"), baz: false),
  item3: Item.new(foo: 10, barred_at: Time.parse_iso8601("2022-01-27T17:45:00Z"), baz: true)
}

puts data.to_yaml

https://carc.in/#/r/cobo

Thanks jhass! Tho sorry if I wasn’t clear, I’m trying to go the other way, i.e. #from_yaml

Works just as well using Hash.from_yaml! :) Carcin

For the record: the proper term for this process would be “deserializing”. I presume that’s what caused the confusion.

Serialization is turning a data structure into a serialized format (a string of characters in this case). Deserialization is the inverse.

Yeah my bad. Thanks for the correction, I did know that, the phrasing in the docs kinda threw me and I had a brainfart.

Actually if you try it with the original YAML file it doesn’t work, it chokes on those times. I think there might be something wrong with our Time::Format::YAML_DATE…

Yeah, I’ve been trying to use #from_yaml but to no avail. I can understand why its failing, but not sure how else to approach it.

Here’s my attempt so far:

record Item, foo : Int32, barred_at : Time, baz : Bool do
  include YAML::Serializable
end

yaml = <<-YAML
---
item1:
  foo: 1
  barred_at: '2022-01-27T17:00:00Z'
  baz: true
item2:
  foo: 5
  barred_at: '2022-01-27T17:30:00Z'
  baz: false
item3:
  foo: 10
  barred_at: '2022-01-27T17:45:00Z'
  baz: true
YAML

items = Array(Item).from_yaml(yaml)

which throws an Unhandled exception: Expected sequence, not mapping at line 2, column 1 (YAML::ParseException). I realise that the YAML represents a hash not an array, so this was never going to work, but hopefully it demonstrates what I’m trying to achieve.

Ultimately I’m hoping to be able to deserialize this YAML into an array of Item structs, which each also include their name (item1, item2, etc.) as a property.

I think the easiest is to just map it over after the fact: Carcin

record ParsedItem, foo : Int32, barred_at : Time, baz : Bool do
  include YAML::Serializable
  
  @[YAML::Field(converter: Time::Format.new("%FT%T%z"))]
  @barred_at : Time
end

record Item, name : String, foo : Int32, barred_at : Time, baz : Bool

items = Hash(String, ParsedItem).from_yaml(yaml)
pp items.map {|name, parsed| Item.new name, parsed.foo, parsed.barred_at, parsed.baz }
2 Likes

Ah, yes, that’s the sort of thing what I was looking for.

Many thanks for your help, much appreciated!