"Hey, can you integrate the new payment API?" my PM asked on a Friday afternoon.
"Sure," I said, before seeing the documentation. The API returned a deeply nested JSON response with 14 different entity types, arrays within arrays, and nullable fields everywhere. Writing the C# models by hand would take the entire afternoon — and it was already 3 PM on a Friday.
I've been there more times than I can count. JSON-to-C# mapping is one of those tasks that sounds simple but eats up hours. The field names are snake_case, your C# conventions want PascalCase. Some fields are null sometimes. Arrays nest inside objects that nest inside other objects.
Let me show you how to handle all of this — and why you should never hand-write these models again.
The Core Problem: Naming Conventions
JSON APIs typically use snake_case or camelCase. C# uses PascalCase for properties. Without conversion, you get ugly code:
// Don't do this — matches JSON but violates C# conventions
public class UserResponse {
public string user_id { get; set; }
public string full_name { get; set; }
public string email_address { get; set; }
}
With proper conversion using System.Text.Json attributes:
using System.Text.Json.Serialization;
public class UserResponse {
[JsonPropertyName("user_id")]
public string UserId { get; set; }
[JsonPropertyName("full_name")]
public string FullName { get; set; }
[JsonPropertyName("email_address")]
public string EmailAddress { get; set; }
}
If you're using Newtonsoft.Json instead:
using Newtonsoft.Json;
public class UserResponse {
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("full_name")]
public string FullName { get; set; }
[JsonProperty("email_address")]
public string EmailAddress { get; set; }
}
A good converter should let you toggle between the two libraries. If your project is .NET Core 3.0+ or .NET 5+, you're likely using System.Text.Json. Older projects and Unity games typically use Newtonsoft.Json.
Handling Nested Objects
Here's a realistic API response from an e-commerce platform:
{
"order": {
"order_id": "ORD-2026-8842",
"created_at": "2026-06-06T10:30:00Z",
"status": "shipped",
"shipping_address": {
"street": "123 Main St",
"city": "Portland",
"state": "OR",
"zip": "97201",
"country": "US"
},
"line_items": [
{
"sku": "TSHIRT-BLK-L",
"name": "Black T-Shirt (Large)",
"quantity": 2,
"unit_price": 29.99,
"total": 59.98
},
{
"sku": "MUG-CERAMIC",
"name": "Ceramic Mug",
"quantity": 1,
"unit_price": 14.99,
"total": 14.99
}
],
"payment": {
"method": "credit_card",
"card_last_four": "4242",
"billing_address": {
"street": "123 Main St",
"city": "Portland",
"state": "OR",
"zip": "97201",
"country": "US"
}
}
}
}
A good converter produces these C# classes:
using System.Text.Json.Serialization;
using System.Collections.Generic;
public class Address {
[JsonPropertyName("street")]
public string Street { get; set; }
[JsonPropertyName("city")]
public string City { get; set; }
[JsonPropertyName("state")]
public string State { get; set; }
[JsonPropertyName("zip")]
public string Zip { get; set; }
[JsonPropertyName("country")]
public string Country { get; set; }
}
public class LineItem {
[JsonPropertyName("sku")]
public string Sku { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("quantity")]
public int Quantity { get; set; }
[JsonPropertyName("unit_price")]
public double UnitPrice { get; set; }
[JsonPropertyName("total")]
public double Total { get; set; }
}
public class Payment {
[JsonPropertyName("method")]
public string Method { get; set; }
[JsonPropertyName("card_last_four")]
public string CardLastFour { get; set; }
[JsonPropertyName("billing_address")]
public Address BillingAddress { get; set; }
}
public class Order {
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("shipping_address")]
public Address ShippingAddress { get; set; }
[JsonPropertyName("line_items")]
public List<LineItem> LineItems { get; set; }
[JsonPropertyName("payment")]
public Payment Payment { get; set; }
}
public class OrderResponse {
[JsonPropertyName("order")]
public Order Order { get; set; }
}
Notice how the converter deduplicated the Address class — both shipping_address and billing_address have the same structure, so a good tool reuses the type instead of generating two identical classes.
Dealing with Nullable Types
Real-world APIs return null more often than you'd expect. Your C# models need to handle this:
{
"user": {
"id": "u_3381",
"name": "Jane Doe",
"email": null,
"phone": null,
"profile_pic": "https://cdn.example.com/avatars/jd.jpg",
"preferences": {
"theme": "dark",
"notifications": null
}
}
}
The generated classes should use nullable annotations:
public class Preferences {
[JsonPropertyName("theme")]
public string Theme { get; set; }
[JsonPropertyName("notifications")]
public object? Notifications { get; set; }
}
public class User {
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("phone")]
public string? Phone { get; set; }
[JsonPropertyName("profile_pic")]
public string ProfilePic { get; set; }
[JsonPropertyName("preferences")]
public Preferences Preferences { get; set; }
}
Key rules for nullable handling:
- Value types (
int,double,DateTime,bool) become nullable when they appear asnullin the JSON:int?,double?,DateTime?,bool? - Reference types (
string,object, custom classes) usestring?syntax with nullable reference types enabled - Arrays that are
nullbecomeList<T>?— but empty arrays[]should beList<T>with a default empty list
I wrote more about handling edge cases in JSON data in the json-format-guide article. The guiding principle is: your deserialization should never throw a NullReferenceException just because an upstream API decided to omit a field.
Working with Arrays and Collections
Array handling trips up a lot of converters. Here's what you should expect:
{
"tags": ["urgent", "high-priority"],
"comments": [],
"attachments": null
}
Correct C# mapping:
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } // Non-null, stores the array
[JsonPropertyName("comments")]
public List<string> Comments { get; set; } // Empty list if empty array
[JsonPropertyName("attachments")]
public List<string>? Attachments { get; set; } // Nullable if JSON has null
Pro tip: In your constructor, initialize collection properties to prevent null reference issues:
public class Order {
public Order() {
LineItems = new List<LineItem>();
Tags = new List<string>();
}
[JsonPropertyName("line_items")]
public List<LineItem> LineItems { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; }
}
This way, even if the JSON omits line_items, your code won't crash when iterating.
Deserializing in Practice
Once you have your models, deserialization is clean:
System.Text.Json (.NET Core 3.0+)
using System.Text.Json;
var json = await httpClient.GetStringAsync("https://api.example.com/orders/8842");
var options = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true
};
var orderResponse = JsonSerializer.Deserialize<OrderResponse>(json, options);
Newtonsoft.Json
using Newtonsoft.Json;
var json = await httpClient.GetStringAsync("https://api.example.com/orders/8842");
var orderResponse = JsonConvert.DeserializeObject<OrderResponse>(json);
Newtonsoft.Json is more forgiving by default — it ignores missing fields and handles casing mismatches. System.Text.Json is stricter and faster, but requires the PropertyNameCaseInsensitive = true option for case-insensitive matching.
Handling Dictionary Responses
Some APIs return dynamic key-value structures instead of fixed schemas:
{
"metadata": {
"environment": "staging",
"build_number": 1423,
"feature_flags": {
"new_checkout": true,
"dark_mode": false,
"beta_search": true
}
}
}
For this, use Dictionary<string, T>:
public class Metadata {
[JsonPropertyName("environment")]
public string Environment { get; set; }
[JsonPropertyName("build_number")]
public int BuildNumber { get; set; }
[JsonPropertyName("feature_flags")]
public Dictionary<string, bool> FeatureFlags { get; set; }
}
// Usage
if (metadata.FeatureFlags.TryGetValue("new_checkout", out var enabled) && enabled) {
// Use new checkout flow
}
Using the Converter Workflow
Here's my current workflow for any new API integration:
- Get a sample JSON response from the API docs or by making a test call
- Paste it into the converter — I use /json-formatter.html for this
- Select C# output with the right library toggle (System.Text.Json / Newtonsoft.Json)
- Copy the generated classes into a new
.csfile - Rename the root class to match my project conventions
- Add any custom attributes like
[Required]or JSON Schema validation
The whole process takes about 30 seconds instead of 30 minutes.
Common Mistakes to Avoid
I've seen these mistakes repeatedly in code reviews:
Mistake 1: Using var for property types
// Wrong — var can't be used for class members
public var Id { get; set; }
// Correct
public string Id { get; set; }
Mistake 2: Forgetting the set accessor
// Serialization won't work without set
public string Name { get; }
// Correct
public string Name { get; set; }
Mistake 3: Using int for nullable number fields
// Wrong — crashes if JSON has "discount": null
public int Discount { get; set; }
// Correct
public int? Discount { get; set; }
Mistake 4: Not initializing collections
// Wrong — line_items might be missing from JSON
public List<LineItem> LineItems { get; set; }
// Usage: order.LineItems.Add(item) throws NullReferenceException
// Correct
public List<LineItem> LineItems { get; set; } = new List<LineItem>();
FAQ
Q: Should I use System.Text.Json or Newtonsoft.Json for new projects?
A: For .NET Core 3.0+ and .NET 5+, System.Text.Json is the default and performs better. Stick with Newtonsoft.Json for Unity projects, Xamarin apps, or legacy .NET Framework code.
Q: How does the converter handle JSON fields that don't match any C# naming convention?
A: Most converters convert snake_case to PascalCase automatically. Fields starting with numbers get prefixed (e.g., "3d_model" becomes _3dModel or Model3D). Fields with special characters are sanitized.
Q: Can I generate record types instead of class types?
A: Some converters support C# 9+ record types. Records are great for immutable DTOs and value equality. If your converter supports it, toggle to record — it produces cleaner code for simple data transfer objects.
Q: What happens when two JSON fields map to the same C# property name?
A: You'll get a duplicate property error. This happens when JSON has both "userName" and "username" — they both want to become UserName. You'll need to manually rename one of them or use a custom [JsonPropertyName] attribute.
Q: How do I handle JSON with polymorphic types (oneOf/anyOf in OpenAPI)?
A: This is tricky. You can use a JsonConverter with custom discriminated handling, or model it as an object? property and deserialize conditionally. Some converters generate a base class with discriminator-based subclasses.
Q: Does the generated code work with both serialization and deserialization?
A: Yes, [JsonPropertyName] and [JsonProperty] attributes work both ways — serialize object to JSON and deserialize JSON to object. The same classes handle both directions.
Q: What about JSON Schema validation attributes?
A: Basic converters don't add validation attributes. You'd add [Required], [StringLength], [Range] manually based on your API contract. Some advanced converters let you import a JSON Schema alongside the sample data.
Q: Can I convert a JSON array directly into List<T>?
A: Yes. If your JSON is a top-level array ([{...}, {...}]), the converter should generate a single class with the array item type, and your deserialization code wraps it in List<ItemType>.
Stop Hand-Writing Models
Next time you're staring at an API response trying to figure out what goes where, let a tool handle the grunt work. Map the JSON once, generate the C# classes, and spend your time on actual business logic instead of property mapping.
Open the JSON Formatter, paste your API response, and generate production-ready C# models in seconds.