DEV Community

Cover image for Manipulating JSON files with Crystal
guto
guto

Posted on

Manipulating JSON files with Crystal

If you ever thought about what it would be like to work with reading JSON files using Crystal, know that this is the right place! Here we will see how we can perform simple manipulations of values in JSON, working with variables, arrays, hashs, among other ways, manipulating our keys with variables!

What is JSON

The name JSON comes from "JavaScript Object Notation", being a compact open standard format for exchanging and manipulating simple data, created in the 2000s (more specifically in 2002) using a key-value format (attribute-value).
Okay, but what would a JSON file format look like?

[
    {
        "message": "talk troop"
    },
    {
        "response": "hey"
    }
]
Enter fullscreen mode Exit fullscreen mode

The keys/attributes would be the fields "message" and "response", in which its value is assigned right after the colon (":"), being separated by braces ("{ }") and a comma indicating the next complete attribute.

Working with Crystal

Opening your working directory, create a new directory to work with this project, in which we will need some "dependencies" before starting:

  • Make sure you have Crystal installed correctly on your machine
  • Prepare the favorite code editor in the directory that was created
  • If you haven't installed Crystal yet click here to learn more! After opening your working directory, if you want to start shards to create a file to control the project, execute in your terminal:
$ shards init
Enter fullscreen mode Exit fullscreen mode

Your shard.yml file should follow the following format:

name: json-reader

version: 0.1.0

authors:
    - João Lanjoni <guto@lanjoni.dev>

description: |
    JSON reader

targets:
    test:
        main: src/main.cr

license: MIT
Enter fullscreen mode Exit fullscreen mode

Now create a directory called src, in it we will put all our code worked using Crystal! Also create a directory called content in the root of the project to contain all our files in JSON format!
Therefore, just add a file in JSON format with the name you want in the content directory and add a main.cr file in the src directory, so we will have our tree:

json-reader/
├── content/
│ └── index.json
├── src/
│ └── main.cr
└── shard.yml
Enter fullscreen mode Exit fullscreen mode

Remembering that shard.yml only exists if you started shards inside your project!

Hands-on (on keyboard)

First, let's create our JSON file, so, following the template above, create a .json file inside the content directory! With it we will manipulate the existing values there!
Now open your src/main.cr file in your favorite code editor so we can better manipulate our project!
Opening your file first let's import the json library to work with files in this format, so add in your code:

require "json"
Enter fullscreen mode Exit fullscreen mode

Every library can be added with the require command!

In order to inform which file will be read, we will perform a simple pass by argument/option when running our project! So, if you want to go a little deeper into passing arguments on the command line with Crystal, click here. In this way, let's save the content of the JSON file in a variable:

content = File.read("content/#{ARGV[0]}")
Enter fullscreen mode Exit fullscreen mode

To receive the first argument/option we will use ARGV[0], after all, position 0 is the first in the array of values passed as options! The content at the beginning means that we only need to pass the file name, after all, a file with the same name will already be searched for in the specified directory!

Right, but there is still the case of the user not adding any option, right? In this case, the code should not even continue, after all, if a JSON file is not specified, then, we cannot carry out the manipulation! So, before that, we add a simple check:

if ARGV.size != 1
    puts "You must pass the filename as a parameter!"
    exit 1
end
Enter fullscreen mode Exit fullscreen mode

The length being different from 1 shows that either more than one argument was passed or none at all, so in both cases we must reject the execution!

There are a few ways to parse our JSON, let's start with the simplest: JSON with unique keys! See the example JSON below:

{
    "test": "hi",
    "test2": "bye"
}
Enter fullscreen mode Exit fullscreen mode

Notice that this format does not have square brackets at the beginning, indicating that it is not an array!

To perform the parse we will use a native Crystal function to read a JSON and then convert it into a Hash of Strings!

hash_content = Hash(String, String).from_json(content)

puts hash_content # {"test" => "hi", "test2" => "bye"}
Enter fullscreen mode Exit fullscreen mode

In this way, we are going to assemble a variable named hash_content, having in its content a hash with a key value from String to String, bringing this data from a JSON

So our final code will look something like this:

require "json"

# Check arguments being equal to 1
if ARGV.size != 1
    puts "You must pass the filename as a parameter!"
    exit 1
end

# Loading the contents of the JSON file
content = File.read("content/#{ARGV[0]}")

# Transforming JSON file data into a String Hash => String
hash_content = Hash(String, String).from_json(content)

# Printing the contents of the variable
puts hash_content # {"test" => "hi", "test2" => "bye"}
Enter fullscreen mode Exit fullscreen mode

We can run it in two ways:

  • Using shards:


    $ shards run -- content.json

  • Using the crystal itself:


    $ crystal run src/main.cr content.json

Okay, but what if we just wanted the test key returned?


puts hash_content["test"] # hi

Enter fullscreen mode Exit fullscreen mode

Thus, we were able to manipulate our data in the JSON file, being able to bring the specific keys for each item!

JSON Arrays

Okay, but most of the JSONs we're going to find need to be "parsed" using Array formats... How can we do that? Well, there are two specific ways to accomplish this task, let's go?

First form: Hash Array

Basically let's assemble an array with its internal type being hashed! But how would that look in practice? Well, first I'll go over the JSON file we'll be working with:

[
    {
        "test": "hello"
    },
    {
        "test2": "hey"
    }
]
Enter fullscreen mode Exit fullscreen mode

Let's first define that we are working with an array, but, when defining the array type, let's change it to Hash(String, String), see:

json = Array(Hash(String, String)).from_json(content)

puts json # [{"test" => "hello"}, {"test2" => "hey"}]
Enter fullscreen mode Exit fullscreen mode

Notice that we've basically wrapped the previous hash format in the array, correct? So we can work with our arrays of JSONs the way we prefer! But, how could I bring only the contents of the "test" key? Simple, see two examples below:

puts json[0] # {"test" => "hello"}

puts json[0]["test"] # hello
Enter fullscreen mode Exit fullscreen mode

So we can manipulate our JSON!
If the key format is not always String, we can still create an alias to manipulate the types passed by Hash or just add a pipe operator ("|") demonstrating that the type can vary, so it would be something like:

Hash(String | Float64, String | Int32)
Enter fullscreen mode Exit fullscreen mode

Second way: File.open

We can also use File.open to open a file and work with its content (in which I leave the credits to cherry for being on live and leaving this very important detail), without the need to create a variable to carry out this work (as we were doing with the content variable). See an implementation below:

json = File.open("content/#{ARGV[0]}") do |file|
    JSON.parse(file)
end

puts json # [{"test" => "hello"}, {"test2" => "hey"}]

puts json[0] # {"test" => "hello"}

puts json[0]["test"] # hello
Enter fullscreen mode Exit fullscreen mode

In this example we perform a simple "parse" of values and update them in the variable named json, being able to work with the values in the same format as before!

Thus, our complete project ends up in the second shape! Here's how our finished code looks:

require "json"

if ARGV.size != 1
    puts "You must pass the filename as a parameter!"
    exit 1
end

json = File.open("content/#{ARGV[0]}") do |file|
    JSON.parse(file)
end

puts json # [{"test" => "hello"}, {"test2" => "hey"}]
Enter fullscreen mode Exit fullscreen mode

Finalization

With this guide and small article you learned how to manipulate JSON files and data coming from other files using Crystal, based on the key-value model that is offered, being able to create commands, specific readings, among other types of projects! To access the code developed in this article just click here!
See you next time, see you later! Crystallize your day even more! 💎 🖤

Top comments (2)

Collapse
 
alexanderadam profile image
Alexander Adam

Nice article!

PS: indention in Crystal is usually 2 spaces 😉

Collapse
 
cherryramatis profile image
Cherry Ramatis

Awesome content! Really like the ability to type check from_json calls