Skip to Content
Course content

247: Practice Exercise: Building a Simple JSON Serializer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start writing serializers in C, the temptation is to treat string manipulation like you would in Python or JavaScript. You just want to "glue" some keys and values together and be done with it. Since we're dealing with JSON—which is essentially just a specific arrangement of quotes, colons, and braces—it feels like a simple job for sprintf.

The "Quick and Dirty" Fixed Buffer

If I were reviewing a junior dev's code and saw something like the example below, my first instinct would be to ask, "What happens when the input grows?"

typedef struct {
    int id;
    char *name;
} User;

char* serialize_user_naive(User *u) {
    char *buffer = malloc(256); // "256 should be plenty, right?"
    sprintf(buffer, "{\"id\": %d, \"name\": \"%s\"}", u->id, u->name);
    return buffer;
}

On the surface, this works. For a user named "Alice," it's perfect. But this is a ticking time bomb. I've seen production systems crash because someone entered a 300-character "name" that overflowed the buffer, leading to a heap corruption that took three days to debug. Even if you swap sprintf for snprintf to prevent the crash, you've just traded a security vulnerability for corrupted data; your JSON will be truncated, and whatever service is consuming that JSON will throw a syntax error because the closing brace is missing.

The Cost of Guessing

The fundamental issue here is that we're guessing the size of the output. In C, guessing is how you get paged at 3 AM. You might think, "I'll just make the buffer 4KB," but that's just delaying the inevitable and wasting memory for every single small request. If you're serializing thousands of these objects in a loop, those wasted bytes add up to a significant memory footprint.

The trade-off we have to make is between simplicity and robustness. The naive approach is simple to write but fragile. The professional approach requires a bit more boilerplate but guarantees that the output is always valid, regardless of the input size.

The Calculate-Allocate-Fill Pattern

The way I handle this in real-world projects is by performing a "dry run" of the serialization. snprintf has a very useful property: if you pass NULL as the buffer and 0 as the size, it doesn't write anything, but it returns the number of characters that would have been written.

I use this to calculate the exact byte count needed, allocate that precise amount of memory, and then perform the actual write. It's a two-pass process, which feels slightly slower, but in the context of I/O and network requests, the CPU cost of a second snprintf call is negligible compared to the cost of a segmentation fault.

char* serialize_user_safe(User *u) {
    // Pass 1: Calculate required length
    int len = snprintf(NULL, 0, "{\"id\": %d, \"name\": \"%s\"}", u->id, u->name);
    
    if (len < 0) return NULL;

    // Allocate exactly what we need (+1 for null terminator)
    char *buffer = malloc(len + 1);
    if (!buffer) return NULL;

    // Pass 2: Actually write the data
    snprintf(buffer, len + 1, "{\"id\": %d, \"name\": \"%s\"}", u->id, u->name);
    
    return buffer;
}

Now, the code is deterministic. Whether the user's name is three characters or three thousand, the serializer will behave exactly the same way. You've traded a few lines of code for total memory safety and data integrity. That's a trade I'll make every single time.




📋 Practical Task

Exercise: Building a Dynamic User-Profile JSON Serializer

Your task is to implement a robust JSON serializer for a UserProfile struct. This struct contains more varied data than our example, meaning you'll need to account for different length requirements.

Requirements:

  • Define a UserProfile struct with the following fields: int user_id, char *username, char *email, and int age.
  • Write a function char* serialize_profile(UserProfile *profile) that uses the "Calculate-Allocate-Fill" pattern to return a valid JSON string.
  • The output format must be: {"id": 123, "user": "name", "email": "email@example.com", "age": 30}.
  • Ensure you handle malloc failures gracefully by returning NULL.
  • In your main function, test your serializer with a "stress case"—a profile where the username and email strings are very long (e.g., 500+ characters) to prove that your dynamic allocation is working correctly.
  • Don't forget to free the allocated string after printing it to avoid memory leaks.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.