vanilla-json

Download Package from NPM, Click to Copy: npm i @rabia_youcef/vanilla-json

Why another JSON engine? Well, I wanted to make something that not only taught me how a set of tools I use daily on my coding journey works, but also takes my skills to the next level. I think building a JSON engine is the perfect project for that. So today, dear reader, I'll teach you how to write your own in JavaScript. And yes, I LOVE JAVASCRIPT (:

Serializer

A JSON serializer turns a data object or structure from a programming language into a text string in JavaScript Object Notation (JSON) format. This string can easily be saved to a file or sent across a network.

{test: "Bonjour"} ----> {"test": "Bonjour"}

{regular JS object} ----> {JSON format for the JS object}

There are actually several cases, and we won't be able to go through them all, but you can write any data in JS and convert it here to get an idea if you've never done it before.

CHECK OUT CONVERTER

Best Part

For now, I think everyone has an idea of what we'll start with. We will implement our own JSON.stringify in JavaScript.

I will write the code first, let you think about it, and then I'll explain every line written.

function stringify(input) {
    let seen = new WeakSet()
    function serialize(input) {
        switch (typeof input) {
            case "string": {
                let str = ""
                for (let i = 0; i < input.length; i++) {
                    if (input[i] === '"') {
                        str += '\\"'
                    } else if (input[i] === "\\") {
                        str += "\\\\"
                    } else if (input[i] === "\n") {
                        str += '\\n'
                    } else if (input[i] === "\t") {
                        str += '\\t'
                    } else if (input[i] === '\r') {
                        str += '\\r'
                    } else if (input[i] === '\b') {
                        str += '\\b'
                    } else if (input[i] === '\f') {
                        str += '\\f'
                    } else if (input.charCodeAt(i) <= 0x1F) {
                        str += '\\u' + input.charCodeAt(i).toString(16).padStart(4, '0')
                    } else {
                        str += input[i]
                    }
                }
                return '"' + str + '"'
            }
        }
    }
}

I stopped here because I had to explain this part very well. Working with strings is easy, until you meet a special character. X)

You might say: "Easy, I'll just mimic the behaviour of the original." So a string containing a newline becomes:

"Hello \n World" ----> "Hello \\n World"

That's smart, GOOD JOB! A quick remark: if the string does not contain any special character, we simply return "the_input_string", and if it does contain special characters, we loop through it and escape them, as I showed above.

After dealing with tab, newline, backspace and so on, are we done? Well, close. We still have around 25 more characters to handle, LOL.

No worries, I didn't know we had that many either, but the happy part is we can deal with all of them using a single condition (including tab). So why did we write the earlier statements at all? Well, I built this from scratch, and I only learned about this catch-all option when I was on version 0.1.2, so I kept the earlier code as legacy. Don't worry, that if statement barely costs any compute.

Strings:

The most important part is dealing with strings!

// you checked the type of input and found it's a String:


let str = ""
for (let i = 0; i < input.length; i++) {

    if (input.charCodeAt(i) <= 0x1F) {
        str += '\\u' + input.charCodeAt(i).toString(16).padStart(4, '0')
    } else {
        str += input[i]
    }
}
return '"' + str + '"'

========================================== COMMENTS ================================================================


// And that's it, we're done dealing with Strings ))
// Okay...
// so what is all this charCodeAt(i), 0x1F???  what is all thisssss
// trust me this is so basic
// input.charCodeAt(i) simply gives me the character code in hex at the given index i
// example string: "Hello World", with i = 0, so input[0] = 0x48, which is simply H in hexadecimal, that's it
// okay, and? what is the point of getting string values in hexadecimal
// hexadecimal is a way to write a numerical value. we humans use decimal
// hexadecimal and binary are ways to represent numerical values in a computer
// and yes, anything represented in a computer is a numerical value
// including that H: 0x48 in decimal is 72
// am I saying all chars, including special chars, are represented as numerical values written in hex or binary? YES
// and more beautifully, they are even organised: all special chars are from 0 to 31
// so all 32 of those chars are special and should be checked? BRAVO
// so the loop simply converts to hex and checks the interval 0 to 31
// красавчик (Krasavchik)!! (Handsome in Russian, but also used as slang for smart)
// then we replace each one with what's needed: a control character like newline (code 10) becomes \\u000a

And obviously if no special character is present we simply build our string as we always do, give it extra quotes (function is called stringify for a reason, I mean you can name it whatever you want just understand how it works).

Quick Remark: Strings in JS are primitive-value type even if they behave like Arrays or Objects, they are not when you use a method on them and it works JS simply creates an Object on the fly that gets deleted later.

Booleans, BigInts, Numbers and Undefined:

This will take 2 minutes to implement literally.

case "boolean": return String(input)
case "bigint" : throw new TypeError("Do not know how to serialize a BigInt")

case "number":
     if (Number.isFinite(input)) {
        return String(input)
     } else {
        return "null"
     }
case "undefined": return undefined

As you have noticed, dear reader, implementation is pretty straightforward. if boolean, convert to string. if BigInt, just throw an error — JS doesn't know how to convert BigInts. if number, well, kinda tricky, but easy: if finite, convert to string and return, but infinite, like +Infinity, and some other cases you can Google. Well, easy — JS doesn't know how to deal with those either, so return null, but inside string literals this time. Don't forget: this is like a black hole — whatever goes inside gets stringified from the other side.))))))

I wanted to mention that undefined, numbers, BigInts, and booleans are also primitive value types.

You might be wondering what other primitive values there are in the JS world. Well, there are Symbols, which I won't mention because I have no idea what they are used for and never had a case where I needed them. And null.

And null — even though if you do typeof(null) you will get "object", that's a bug. In reality, it's a type of its own, and it's a primitive value type. Like typeof(5) === "number", in reality typeof(null) === "null". Read more here.

Now, the last part of our project serializer

Objects:

case "object":
    if (input === null) {
        return "null"
    }

    else if (typeof input.toJSON === "function") {
        return serialize(input.toJSON());
    } else if (Array.isArray(input)) {
        if (seen.has(input)) throw new TypeError("Converting circular structure to JSON")  // ← guard
        seen.add(input)                                                                    // ← "I'm inside now"

        if (input.length > 0) {
            let str = "["
            for (let i = 0; i < input.length; i++) {
                str += (serialize(input[i]) ?? "null") + ","
            }
            seen.delete(input)                                                             // ← "done, leaving"
            return str.slice(0, -1) + "]"
        } else {
            seen.delete(input)                                                             // ← same, empty case
            return "[]"
        }

    } else {
        if (seen.has(input)) throw new TypeError("Converting circular structure to JSON")  // ← guard
        seen.add(input)                                                                    // ← "I'm inside now"

        let key_s = Object.keys(input)
        let str = "{"
        for (let i = 0; i < key_s.length; i++) {
            let piece = serialize(input[key_s[i]])
            if (piece === undefined) continue
            str += '"' + key_s[i] + '":' + piece + ","
        }

        seen.delete(input)                                                                 // ← "done, leaving"
        if (str !== "{") {
            return str.slice(0, -1) + "}"
        } else {
            return "{}"
        }
    }
}

return serialize(input)

First, we check what type of object we are dealing with. If it's null (bug we mentioned before that typeof(null) expression results in object), anyway, that's simple: we return "null".

In the next if statement, you noticed I checked our input.toJSON if it is a function. I serialize what that function returns. What does that even mean???

Well, it's so simple. It's just a method you implement on objects which you can also find implemented on built-in objects like Date objects and so on (JSON method).

What is the use case of it? Well, we said when we find it, we serialize what it returns! Okay, and why? So the serializer serialises what we want, not everything. Imagine if one of your object properties is password: you don't want that serialized and sent over the Internet for sure, lol.

This technique is called duck typing or protocol-based design in JS because it changes from one language to another. For instance, in C++, duck typing is implemented differently.

The second case is arrays, and yes, arrays are objects, not a special type. So sad. Actually, it's cool. That's why I love JS: functions are a type and arrays are not, lol. But fun fact: under the hood, functions are special objects. So, is it true that everything in JS is an object? No, that's a stereotype. JS is amazing!!!!

Don't pay any attention to WeakSet or seen: that's to avoid circular references, another whole universe. If you like this article, I might make one about that HOT TOPIC, hehe.

As you noticed, I built an array and serialized every element, and then returned it inside quotes, and did the same thing for objects? Wow, that is it. That's the explanation. Well, yes, because that is really all I did, lol. Check the type, open { or [, and serialize the content for arrays and values for objects, close the array and return the keys with quotes and their serialized values, close the object too. Return. If empty, return "[]" or "{}". And that is it.

Make sure to READ CODE, and you, my friend, have successfully built your SERIALIZER in pure JS.