C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
206: JsonSerializer Options and Attributes
When you first start working with System.Text.Json, it feels like magic. You pass a class into the serializer, and it spits out a string. But that magic breaks the moment you have to integrate with a real-world API. Most external APIs use snake_case or camelCase, while we, as C# developers, adhere to PascalCase for our properties. This creates a friction point that many developers handle the wrong way.
The temptation to break C# naming conventions
I've seen a lot of juniors solve this by simply naming their C# properties to match the JSON. They'll write a class like this:
public class UserProfile
{
public string first_name { get; set; }
public string last_name { get; set; }
public string email_address { get; set; }
}
I'll be honest: it works. The JSON matches perfectly. But you've just poisoned your codebase. Now, every time you use this object in your business logic, you're writing user.first_name instead of user.FirstName. You're fighting the language and your IDE's static analysis just to satisfy a JSON string. It's a short-term win that creates long-term technical debt. You should never let an external data format dictate your internal C# naming standards.
Solving the bulk of the problem with global options
The better way to handle this is to decouple your C# names from the JSON keys using JsonSerializerOptions. If your API is consistently using camelCase, you don't need to touch your classes at all. You just tell the serializer how to behave globally.
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
string json = JsonSerializer.Serialize(user, options);
This is the "clean" approach. Your C# properties stay PascalCase, but the output becomes camelCase. It's a single line of configuration that handles every property in your object graph. However, this approach breaks down the moment you hit a "special" field. What happens when the API uses user_id (snake_case) but the rest is camelCase? Or what if a field is named id in JSON but you want it called InternalAccountId in your code for clarity?
Precision control with attributes
This is where attributes come in. Think of JsonSerializerOptions as the broad brush and attributes as the scalpel. By using [JsonPropertyName], you can override the global policy for a specific property. This allows you to maintain a beautiful C# domain model while remaining perfectly compatible with a messy API.
public class UserProfile
{
[JsonPropertyName("user_id")]
public int UserId { get; set; }
[JsonPropertyName("first_name")]
public string FirstName { get; set; }
public string Email { get; set; } // Will follow the global PropertyNamingPolicy
}
I usually recommend using global options for the 90% use case and attributes for the exceptions. It keeps your configuration lean and your intentions explicit. While you're at it, you'll likely encounter data you don't want to serialize—like a password hash or a sensitive internal state. Instead of making that property internal or private (which can complicate your constructors), use [JsonIgnore]. It tells the serializer to completely skip that property, ensuring you don't accidentally leak secrets to a client-side application.
The cost of flexibility
There is a small trade-off here: tight coupling. When you use [JsonPropertyName], your C# class now "knows" about the JSON format. If the API changes a key from first_name to given_name, you have to update your code and recompile. For most applications, this is acceptable. If you're building something that needs to be extremely dynamic—where the keys change based on user configuration—you'd need to move toward a Dictionary<string, object> or a custom JsonConverter, but that's a level of complexity you should avoid until you actually need it.
📋 Practical Task
Refactoring the Legacy Product Catalog
You have been handed a Product class that was written by a developer who ignored C# naming conventions to make the JSON "just work." Your task is to refactor this class and the serialization logic to follow professional standards.
The current (bad) code:
public class Product
{
public string product_sku { get; set; }
public string display_name { get; set; }
public decimal unit_price { get; set; }
public string internal_warehouse_code { get; set; }
}
// Usage
var product = new Product { product_sku = "ABC-123", display_name = "Widget", unit_price = 10.99m, internal_warehouse_code = "WH-01" };
string json = JsonSerializer.Serialize(product);
Requirements:
- Rename all properties to
PascalCase(e.g.,ProductSku). - Use
[JsonPropertyName]to ensure the JSON output still uses thesnake_casekeys (e.g.,product_sku). - The
internal_warehouse_codeproperty must be completely omitted from the resulting JSON string using the appropriate attribute. - Configure
JsonSerializerOptionsto enableWriteIndented = trueso the output is human-readable.
There are no comments for now.