"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 as null in the JSON: int?, double?, DateTime?, bool?
  • Reference types (string, object, custom classes) use string? syntax with nullable reference types enabled
  • Arrays that are null become List<T>? — but empty arrays [] should be List<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:

  1. Get a sample JSON response from the API docs or by making a test call
  2. Paste it into the converter — I use /json-formatter.html for this
  3. Select C# output with the right library toggle (System.Text.Json / Newtonsoft.Json)
  4. Copy the generated classes into a new .cs file
  5. Rename the root class to match my project conventions
  6. 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.