C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
247: Practice Exercise: Building a Simple JSON Serializer
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
UserProfilestruct with the following fields:int user_id,char *username,char *email, andint 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
mallocfailures gracefully by returningNULL. - In your
mainfunction, 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
freethe allocated string after printing it to avoid memory leaks.
There are no comments for now.