# HttpClient and JSON

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Advanced Development
Source: https://www.devtutorials.ai/learn/advanced-csharp/advanced-development/httpclient-json

> Call web APIs and handle JSON: HttpClient, GET, POST, PUT and DELETE, headers, responses, System.Text.Json serialization, options, attributes, typed clients and error handling.

## Introduction to HttpClient

Think about ordering food through a delivery app. You send a request like "one masala dosa to this address". The restaurant sends back a response—maybe the food, or maybe "sorry, sold out". Both sides understand the same message format, so it doesn't matter that the app and the kitchen use completely different systems.

Programs talk to each other the same way, using **HTTP**: the language of the web. Your C# program sends an HTTP **request** to a **web API**, and the API sends back an HTTP **response**. Most web APIs send their data as **JSON**, which is just plain text that follows a simple format both sides understand.

Here is what you need to know:

- **`HttpClient`** is the C# class that sends HTTP requests and gets responses back.
- **`System.Text.Json`** is the C# library that turns your objects into JSON and turns JSON back into your objects.

**In simple words:** `HttpClient` talks to web APIs over the internet; `System.Text.Json` translates between JSON text and C# objects.

**Easy Way to Remember:** **HttpClient carries the message. JSON is the language the message is written in.**

## What You Will Learn

In this lesson, you will learn:

1. How to create and set up `HttpClient`.
2. How to send GET, POST, PUT, and DELETE requests.
3. How to add request headers (extra information with requests).
4. How to read status codes and response bodies (the data that comes back).
5. How to turn C# objects into JSON and turn JSON back into C# objects.
6. How to change how JSON works using options and attributes.
7. How to build a simple API client class that wraps an API.
8. How to handle errors that happen when calling web APIs.

The examples you can run here use a **pretend web API** built into the program: a small class that answers requests the way a real server would. This way every example works without needing the internet, and you get the same result every time. The `HttpClient` code is exactly what you would write with a real API. When we show real code that uses the internet, we'll make it clear.

## Start With a Working Example

This is how you read data from a real web API. It needs an internet connection, so this is just plain code (you can't run it here):

```text
using System.Net.Http.Json;

using var client = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };

Product? product = await client.GetFromJsonAsync<Product>("products/1");
Console.WriteLine($"{product?.Name}: Rs. {product?.Price}");

record Product(int Id, string Name, decimal Price);
```

Now here is the same code using the pretend API built into this page. Click **Run** to see it work.

```csharp runnable title="Get a product from an API"
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeShopApi()) { BaseAddress = new Uri("https://api.example.com/") };

Product? product = await client.GetFromJsonAsync<Product>("products/1");
Console.WriteLine($"{product?.Name}: Rs. {product?.Price}");

record Product(int Id, string Name, decimal Price);

// Pretend server: answers requests without using the network.
class FakeShopApi : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Console.WriteLine($"(server got {request.Method} {request.RequestUri!.PathAndQuery})");
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(new { id = 1, name = "Electric Kettle", price = 1299.00m }),
        };
        return Task.FromResult(response);
    }
}
```

One single line, `GetFromJsonAsync<Product>`, did three things:
1. Sent a GET request
2. Checked if the response was successful
3. Turned the JSON data into a `Product` object

### How Does the Pretend API Work?

`HttpClient` doesn't talk to the network by itself. Instead, it passes each request to a **message handler**. The default handler opens a real internet connection. But here, `FakeShopApi` is a handler that answers the request right in memory instead.

This is also how professionals **test** code that uses `HttpClient`: they give it a fake handler that returns fake data. This way they can test without needing a real server.

## Creating HTTP Requests

Every HTTP request has four parts:

| Part | Example | What It Does |
|---|---|---|
| **Method** | `GET`, `POST`, `PUT`, `DELETE` | Says what action you want |
| **URL** | `https://api.example.com/products/1` | Says which resource you want |
| **Headers** | `Accept: application/json` | Extra info about what you're asking for |
| **Body** (optional) | `{"name":"Pen","price":10}` | Data you are sending to the server |

### Creating HttpClient the Right Way

`HttpClient` is designed to be **created once and used many times**. If you create a new `HttpClient` for every request, your program uses up network connections. Eventually your program will run out of connections. This problem is called **socket exhaustion**.

```text
// Wrong: a new client (and new connections) for every request
foreach (var id in ids)
{
    using var client = new HttpClient();
    await client.GetAsync($"https://api.example.com/products/{id}");
}

// Right for a console program: create one client and reuse it
private static readonly HttpClient Client = new() { BaseAddress = new Uri("https://api.example.com/") };
```

In ASP.NET Core and apps that use dependency injection, use **`IHttpClientFactory`** (see lesson 17). The factory manages connections automatically:

```text
builder.Services.AddHttpClient<ProductApiClient>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.Timeout = TimeSpan.FromSeconds(10);
});

public class ProductApiClient(HttpClient http)   // the factory gives you a configured client
{
    public Task<Product?> GetAsync(int id) => http.GetFromJsonAsync<Product>($"products/{id}");
}
```

### Building a Request by Hand

The helper methods like `GetAsync`, `PostAsync`, etc. work for most situations. But if you need more control, you can build an **`HttpRequestMessage`** yourself and send it with **`SendAsync`**:

```csharp runnable title="Build a request by hand"
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new EchoServer()) { BaseAddress = new Uri("https://api.example.com/") };

var request = new HttpRequestMessage(HttpMethod.Get, "orders?status=shipped&page=2");
request.Headers.Add("X-Request-Id", "req-7781");

HttpResponseMessage response = await client.SendAsync(request);
Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");

class EchoServer : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Console.WriteLine($"Method: {request.Method}");
        Console.WriteLine($"Path:   {request.RequestUri!.AbsolutePath}");
        Console.WriteLine($"Query:  {request.RequestUri.Query}");
        Console.WriteLine($"Header: X-Request-Id = {string.Join(",", request.Headers.GetValues("X-Request-Id"))}");
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}
```

When you set `BaseAddress` and give a relative URL, they combine into the full address. Keep the `/` at the end of `BaseAddress`, and don't put a `/` at the start of the relative path.

## GET Requests

**GET** means "give me data". GET requests never change anything on the server—they only read data.

There are three common ways to send a GET request:

| Method | Returns | When to use |
|---|---|---|
| `GetFromJsonAsync<T>(url)` | `T?` | When you want an object back, and error status codes should throw an exception |
| `GetStringAsync(url)` | `string` | When you want the raw text response |
| `GetAsync(url)` | `HttpResponseMessage` | When you want to check the status code and headers yourself |

```csharp runnable title="Get a list as JSON"
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeShopApi()) { BaseAddress = new Uri("https://api.example.com/") };

List<Product>? products = await client.GetFromJsonAsync<List<Product>>("products?category=kitchen");
foreach (var p in products!)
    Console.WriteLine($"{p.Id}  {p.Name,-16} Rs. {p.Price}");

string raw = await client.GetStringAsync("products?category=kitchen");
Console.WriteLine($"Raw JSON starts with: {raw[..40]}...");

record Product(int Id, string Name, decimal Price);

class FakeShopApi : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        var items = new[]
        {
            new { id = 1, name = "Electric Kettle", price = 1299m },
            new { id = 2, name = "Mixer Grinder", price = 3499m },
            new { id = 3, name = "Toaster", price = 1599m },
        };
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(items) });
    }
}
```

## POST Requests

**POST** means "here is new data, please create something with it". POST requests send data to the server, usually to create a new item. The data goes in the request **body**.

**`PostAsJsonAsync(url, value)`** takes your object, turns it into JSON, sets the `Content-Type: application/json` header (so the server knows it's JSON), and sends it.

When a POST succeeds, the server usually responds with **201 Created**, and often sends back the new item including its ID.

```csharp runnable title="Create an order with POST"
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeOrderApi()) { BaseAddress = new Uri("https://api.example.com/") };

var newOrder = new CreateOrder("Asha", "Electric Kettle", 2);
HttpResponseMessage response = await client.PostAsJsonAsync("orders", newOrder);

Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"Location: {response.Headers.Location}");

Order? created = await response.Content.ReadFromJsonAsync<Order>();
Console.WriteLine($"Created {created?.Id} for {created?.Customer}: {created?.Quantity} x {created?.Item}");

record CreateOrder(string Customer, string Item, int Quantity);
record Order(string Id, string Customer, string Item, int Quantity);

class FakeOrderApi : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        Console.WriteLine($"Server received: {await request.Content!.ReadAsStringAsync(token)}");
        var input = await request.Content.ReadFromJsonAsync<CreateOrder>(token);
        var order = new Order("ORD-9001", input!.Customer, input.Item, input.Quantity);

        var response = new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(order) };
        response.Headers.Location = new Uri("https://api.example.com/orders/ORD-9001");
        return response;
    }
}
```

Look at what the server received: `{"customer":"Asha","item":"Electric Kettle","quantity":2}`. The `...AsJsonAsync` helpers use **web conventions**, which turn your property names into **camelCase** (first letter small, then capital letters for each new word). Most web APIs expect this format.

## PUT Requests

**PUT** means "replace this item with what I'm sending". You send the **complete** updated item to its URL.

(There is also **PATCH**, which means "change just some fields". It works the same way as PUT but you only send the fields you're changing. Use `PatchAsJsonAsync` the same way you use `PutAsJsonAsync`.)

```csharp runnable title="Update a product with PUT"
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeProductApi()) { BaseAddress = new Uri("https://api.example.com/") };

var updated = new Product(2, "Mixer Grinder 750W", 3299m);
HttpResponseMessage response = await client.PutAsJsonAsync("products/2", updated);

Console.WriteLine($"PUT status: {(int)response.StatusCode} {response.StatusCode}");

record Product(int Id, string Name, decimal Price);

class FakeProductApi : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        var product = await request.Content!.ReadFromJsonAsync<Product>(token);
        Console.WriteLine($"Server: {request.Method} {request.RequestUri!.AbsolutePath} -> {product!.Name} at Rs. {product.Price}");
        return new HttpResponseMessage(HttpStatusCode.NoContent);   // done, nothing to return
    }
}
```

Notice the response is **204 No Content**. This means "the update worked, and there's no data body to send back". This is a common response to PUT and DELETE requests.

## DELETE Requests

**DELETE** removes an item. It usually has no body—the URL tells the server which item to delete.

```csharp runnable title="Delete an item"
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeCartApi()) { BaseAddress = new Uri("https://api.example.com/") };

foreach (string id in new[] { "item-7", "item-99" })
{
    HttpResponseMessage response = await client.DeleteAsync($"cart/{id}");
    Console.WriteLine($"DELETE {id}: {(int)response.StatusCode} {response.StatusCode}");
}

class FakeCartApi : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        bool exists = request.RequestUri!.AbsolutePath.EndsWith("item-7");
        return Task.FromResult(new HttpResponseMessage(exists ? HttpStatusCode.NoContent : HttpStatusCode.NotFound));
    }
}
```

Here's a summary of the four main HTTP methods:

| Method | What It Does | Sends a Body? | Typical Success Code |
|---|---|---|---|
| `GET` | Read data | No | 200 OK |
| `POST` | Create a new item | Yes | 201 Created |
| `PUT` | Replace an entire item | Yes | 200 OK or 204 No Content |
| `PATCH` | Change just some fields | Yes | 200 OK or 204 No Content |
| `DELETE` | Remove an item | Usually no | 204 No Content |

## Request Headers

**Headers** are extra information you send with a request: who you are, what format you want back, which language you prefer, etc.

You can put headers in two places:

- **`client.DefaultRequestHeaders`**: these go with **every** request from that client. Good for an API key or an `Accept` header that you always use.
- **`request.Headers`** on one `HttpRequestMessage`: these go with **just that one** request.

```csharp runnable title="Default and per-request headers"
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new HeaderPrinter()) { BaseAddress = new Uri("https://api.example.com/") };

client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "demo-token-123");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyShopApp/1.0");

var request = new HttpRequestMessage(HttpMethod.Get, "profile");
request.Headers.AcceptLanguage.ParseAdd("ta-IN");

await client.SendAsync(request);

class HeaderPrinter : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        foreach (var header in request.Headers.OrderBy(h => h.Key))
            Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}
```

Never write an API key or password in your code. Read it from a settings file or a secret store (see lesson 18), and set it once on the client. Also, never include the `Authorization` header in log files (see lesson 23).

## Response Handling

A response has three main parts that matter:

- **`StatusCode`**: a number that tells you what happened.
- **`Headers`** and **`Content.Headers`**: extra information, like `Content-Type` (what format the body is in).
- **`Content`**: the body—the actual data, which you read as text, a stream, or JSON.

**Status codes** come in groups:

| Range | What It Means | Examples |
|---|---|---|
| **2xx** | Success | 200 OK, 201 Created, 204 No Content |
| **3xx** | Redirect (go somewhere else) | 301 Moved Permanently, 304 Not Modified |
| **4xx** | **Your** request has a problem | 400 Bad Request, 401 Unauthorized, 404 Not Found, 429 Too Many Requests |
| **5xx** | The **server** has a problem | 500 Internal Server Error, 503 Service Unavailable |

**`response.IsSuccessStatusCode`** is `true` if the status is any 2xx code.

```csharp runnable title="Check the status before reading the body"
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FakeShopApi()) { BaseAddress = new Uri("https://api.example.com/") };

foreach (int id in new[] { 1, 42 })
{
    HttpResponseMessage response = await client.GetAsync($"products/{id}");

    if (response.IsSuccessStatusCode)
    {
        var product = await response.Content.ReadFromJsonAsync<Product>();
        Console.WriteLine($"Product {id}: {product!.Name} ({response.Content.Headers.ContentType?.MediaType})");
    }
    else if (response.StatusCode == HttpStatusCode.NotFound)
    {
        Console.WriteLine($"Product {id}: not found");
    }
    else
    {
        Console.WriteLine($"Product {id}: failed with {(int)response.StatusCode}");
    }
}

record Product(int Id, string Name, decimal Price);

class FakeShopApi : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
    {
        if (request.RequestUri!.AbsolutePath == "/products/1")
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
            { Content = JsonContent.Create(new Product(1, "Electric Kettle", 1299m)) });

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}
```

When you get a large response, use `using var response = await client.GetAsync(...)` to clean up the `HttpResponseMessage`. For very big downloads, read the body as a **stream** with `ReadAsStreamAsync`, so the entire file doesn't have to fit in memory at once.

## JSON Serialization

**Serialization** is turning a C# object into JSON text so you can send it or store it. **`JsonSerializer.Serialize`** does this.

Here's how C# types map to JSON:

| C# | JSON |
|---|---|
| Object or record | `{ "name": value, ... }` |
| `List<T>`, array | `[ ... ]` |
| `string` | `"text"` |
| `int`, `decimal`, `double` | `42`, `1299.5` |
| `bool` | `true` / `false` |
| `null` | `null` |
| `DateTime` | `"2025-08-15T10:30:00"` (ISO 8601 format) |

```csharp runnable title="Turn objects into JSON"
using System;
using System.Collections.Generic;
using System.Text.Json;

var order = new Order(
    Id: "ORD-2201",
    Customer: "Asha",
    PlacedOn: new DateTime(2025, 8, 15, 10, 30, 0),
    Items: new List<Line> { new("Kettle", 1, 1299m), new("Mug", 4, 199m) },
    GiftWrap: false,
    Note: null);

string json = JsonSerializer.Serialize(order);
Console.WriteLine(json);

record Line(string Product, int Qty, decimal Price);
record Order(string Id, string Customer, DateTime PlacedOn, List<Line> Items, bool GiftWrap, string? Note);
```

By default, the property names stay **exactly the same as in C#** (using PascalCase—first letter capital). The output is also on **one long line**. Options (explained below) can change both of these.

## JSON Deserialization

**Deserialization** is the opposite: turning JSON text into a C# object. **`JsonSerializer.Deserialize<T>`** does this.

The serializer reads the JSON, matches the JSON names to your property names, converts the values to the right types, and builds the object. Both records with constructors and classes with settable properties work.

```csharp runnable title="Turn JSON into objects"
using System;
using System.Text.Json;

string json = """
{
  "Id": "ORD-3310",
  "Customer": "Ravi",
  "Total": 2450.75,
  "Paid": true,
  "Tags": ["express", "gift"],
  "ShipTo": { "City": "Chennai", "Pin": "600001" },
  "Unknown": "ignored"
}
""";

Order? order = JsonSerializer.Deserialize<Order>(json);

Console.WriteLine($"{order!.Id} for {order.Customer}, Rs. {order.Total}, paid: {order.Paid}");
Console.WriteLine($"Tags: {string.Join(", ", order.Tags)}");
Console.WriteLine($"Ship to: {order.ShipTo.City} {order.ShipTo.Pin}");

record Address(string City, string Pin);
record Order(string Id, string Customer, decimal Total, bool Paid, string[] Tags, Address ShipTo);
```

Notice that `"Unknown"` didn't cause an error—it was just **ignored**, because `Order` doesn't have an `Unknown` property. Properties that are missing from the JSON get their **default value** (0 for numbers, null for nullable types, etc.). You can change both behaviors with options.

### When the JSON Doesn't Match

Bad JSON, or a value that can't be turned into the right type, throws a **`JsonException`** and tells you the line and position where the problem is:

```csharp runnable title="Bad JSON throws JsonException"
using System;
using System.Text.Json;

string[] samples =
{
    """{ "Name": "Pen", "Price": 10 }""",
    """{ "Name": "Pen", "Price": "ten" }""",
    """{ "Name": "Pen", "Price": 10 """,
};

foreach (string json in samples)
{
    try
    {
        var item = JsonSerializer.Deserialize<Item>(json);
        Console.WriteLine($"OK: {item!.Name} at {item.Price}");
    }
    catch (JsonException ex)
    {
        Console.WriteLine($"JsonException: {ex.Message.Split('.')[0]}.");
    }
}

record Item(string Name, decimal Price);
```

## System.Text.Json

**`System.Text.Json`** is the built-in JSON library in .NET. It's fast, uses very little memory, and works great with `HttpClient`. The older **Newtonsoft.Json** (Json.NET) library is still used in many existing projects, but new code should use `System.Text.Json`.

Besides `JsonSerializer`, it has two more tools for when you don't have a class for the JSON shape:

- **`JsonDocument`**: read-only and very fast. Use this when you just need to pull a few values out of a large JSON document.
- **`JsonNode`**: an editable tree. Use this when you need to read **and** change the JSON.

```csharp runnable title="Read and change JSON without a class"
using System;
using System.Text.Json;
using System.Text.Json.Nodes;

string json = """{ "user": { "name": "Meera", "points": 120 }, "level": "silver" }""";

// JsonDocument: fast, read-only
using (JsonDocument doc = JsonDocument.Parse(json))
{
    JsonElement user = doc.RootElement.GetProperty("user");
    Console.WriteLine($"Name: {user.GetProperty("name").GetString()}, points: {user.GetProperty("points").GetInt32()}");
}

// JsonNode: you can change it
JsonNode root = JsonNode.Parse(json)!;
root["user"]!["points"] = 250;
root["level"] = "gold";
root["upgradedOn"] = "2025-09-01";

Console.WriteLine(root.ToJsonString());
```

## JSON Options

**`JsonSerializerOptions`** changes how JSON is written and read. Here are the most useful settings:

| Option | What It Does |
|---|---|
| `PropertyNamingPolicy = JsonNamingPolicy.CamelCase` | Turns `CustomerName` into `customerName` in JSON |
| `WriteIndented = true` | Adds line breaks and spaces to make JSON pretty |
| `PropertyNameCaseInsensitive = true` | Reads `"NAME"`, `"name"` or `"Name"` all into the same property |
| `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull` | Doesn't include properties that are `null` |
| `Converters = { new JsonStringEnumConverter() }` | Writes enums as `"Shipped"` instead of the number `2` |
| `AllowTrailingCommas`, `ReadCommentHandling` | Accept slightly messy JSON that has trailing commas or comments |

**`JsonSerializerOptions.Web`** is a ready-made set for web APIs: camelCase names and case-insensitive reading. `HttpClient` and ASP.NET Core use this by default.

```csharp runnable title="Options change the JSON"
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

var shipment = new Shipment("SHP-77", "Kiran Kumar", Status.InTransit, Note: null);

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    Converters = { new JsonStringEnumConverter() },
};

Console.WriteLine("Default:");
Console.WriteLine(JsonSerializer.Serialize(shipment));
Console.WriteLine("With options:");
Console.WriteLine(JsonSerializer.Serialize(shipment, options));

var back = JsonSerializer.Deserialize<Shipment>("""{"ID":"SHP-78","CUSTOMERNAME":"Asha","status":"Delivered"}""",
    new JsonSerializerOptions(options) { PropertyNameCaseInsensitive = true });
Console.WriteLine($"Read back: {back!.Id}, {back.CustomerName}, {back.Status}");

enum Status { Packed, InTransit, Delivered }
record Shipment(string Id, string CustomerName, Status Status, string? Note);
```

Create a `JsonSerializerOptions` object **once** and reuse it, such as in a `static readonly` field. Every new options object rebuilds an internal cache, which is slow. If you make a new one on every call, your code will be slow.

## JSON Attributes

Options change behavior for **all types**. **Attributes** change behavior for **one specific property or type**. Attributes are in `System.Text.Json.Serialization`.

| Attribute | What It Does |
|---|---|
| `[JsonPropertyName("first_name")]` | Uses a different name in the JSON |
| `[JsonIgnore]` | Never write or read this property |
| `[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]` | Skip this property only when it is `null` |
| `[JsonPropertyOrder(1)]` | Control which order properties appear in |
| `[JsonRequired]` | Throw an error if this property is missing from the JSON |
| `[JsonConverter(typeof(JsonStringEnumConverter))]` | Use a custom way to read/write this type |
| `[JsonInclude]` | Include a private property or field in the JSON |

```csharp runnable title="Attributes shape the JSON for one type"
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

var user = new UserProfile
{
    Id = 501,
    FirstName = "Priya",
    Email = "priya@example.com",
    Password = "s3cret!",
    Plan = Plan.Premium,
};

string json = JsonSerializer.Serialize(user, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

try
{
    JsonSerializer.Deserialize<UserProfile>("""{ "first_name": "Ravi" }""");
}
catch (JsonException ex)
{
    Console.WriteLine($"Error: {ex.Message.Split('.')[0]}.");
}

enum Plan { Free, Premium }

class UserProfile
{
    [JsonPropertyOrder(-1)]
    [JsonRequired]
    [JsonPropertyName("user_id")]
    public int Id { get; set; }

    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } = "";

    public string Email { get; set; } = "";

    [JsonIgnore]
    public string Password { get; set; } = "";         // never sent as JSON

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public Plan Plan { get; set; }
}
```

Notice that the password never appears in the JSON. The `Id` appears first and is named `user_id`. If the JSON doesn't have `user_id`, reading fails because it's `[JsonRequired]`.

## Working with APIs

In a real app, don't scatter `HttpClient` calls all over your code. Instead, wrap one API in **one typed client class**. Its methods speak in your C# types, and only it knows about URLs and JSON details.

```csharp runnable title="A typed API client"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

var http = new HttpClient(new FakeTodoApi()) { BaseAddress = new Uri("https://api.example.com/") };
var todos = new TodoClient(http);

await todos.AddAsync("Pay electricity bill");
await todos.AddAsync("Book train tickets");
await todos.CompleteAsync(1);

foreach (var t in await todos.ListAsync())
    Console.WriteLine($"[{(t.Done ? "x" : " ")}] {t.Id}. {t.Title}");

record Todo(int Id, string Title, bool Done);
record NewTodo(string Title);

class TodoClient
{
    private readonly HttpClient _http;
    public TodoClient(HttpClient http) => _http = http;

    public async Task<List<Todo>> ListAsync() =>
        await _http.GetFromJsonAsync<List<Todo>>("todos") ?? new();

    public async Task<Todo> AddAsync(string title)
    {
        var response = await _http.PostAsJsonAsync("todos", new NewTodo(title));
        response.EnsureSuccessStatusCode();
        return (await response.Content.ReadFromJsonAsync<Todo>())!;
    }

    public async Task CompleteAsync(int id)
    {
        var response = await _http.PutAsJsonAsync($"todos/{id}/done", new { });
        response.EnsureSuccessStatusCode();
    }
}

// Pretend API that remembers the to-do list in memory.
class FakeTodoApi : HttpMessageHandler
{
    private readonly List<Todo> _items = new();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken token)
    {
        string path = req.RequestUri!.AbsolutePath;
        if (req.Method == HttpMethod.Get && path == "/todos")
            return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(_items) };

        if (req.Method == HttpMethod.Post && path == "/todos")
        {
            var input = await req.Content!.ReadFromJsonAsync<NewTodo>(token);
            var todo = new Todo(_items.Count + 1, input!.Title, false);
            _items.Add(todo);
            return new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(todo) };
        }

        if (req.Method == HttpMethod.Put && path.EndsWith("/done"))
        {
            int id = int.Parse(path.Split('/')[2]);
            int index = _items.FindIndex(t => t.Id == id);
            _items[index] = _items[index] with { Done = true };
            return new HttpResponseMessage(HttpStatusCode.NoContent);
        }

        return new HttpResponseMessage(HttpStatusCode.NotFound);
    }
}
```

The rest of your code uses `TodoClient` like any other class. It never sees URLs, status codes, or JSON. If you want to switch to a different API, you only change this one class.

## HTTP Error Handling

Network calls fail. Servers crash, internet connections drop, requests take too long. Good code expects this to happen and handles it.

### The Errors You Will See

| What Happens | What You Get |
|---|---|
| Server returns 4xx or 5xx | No exception; `GetAsync` returns a response with status in `StatusCode` |
| Call `EnsureSuccessStatusCode()` on an error | Throws `HttpRequestException` with `StatusCode` set |
| `GetFromJsonAsync` on an error status | Throws `HttpRequestException` |
| DNS fails, connection refused, etc. | Throws `HttpRequestException` (no status code) |
| Request takes longer than `Timeout` (default 100 seconds) | Throws `TaskCanceledException` |
| Body is not valid JSON | Throws `JsonException` |

```csharp runnable title="Handle each kind of failure"
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new UnreliableApi())
{
    BaseAddress = new Uri("https://api.example.com/"),
    Timeout = TimeSpan.FromMilliseconds(200),
};

foreach (string path in new[] { "ok", "missing", "broken", "slow", "garbage" })
{
    try
    {
        var data = await client.GetFromJsonAsync<Reply>(path);
        Console.WriteLine($"{path,-8} -> success: {data!.Message}");
    }
    catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
    {
        Console.WriteLine($"{path,-8} -> not found (404)");
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"{path,-8} -> HTTP error {(int?)ex.StatusCode}");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine($"{path,-8} -> timed out");
    }
    catch (JsonException)
    {
        Console.WriteLine($"{path,-8} -> response was not valid JSON");
    }
}

record Reply(string Message);

class UnreliableApi : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken token)
    {
        switch (req.RequestUri!.AbsolutePath)
        {
            case "/ok":
                return new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new Reply("all good")) };
            case "/missing":
                return new HttpResponseMessage(HttpStatusCode.NotFound);
            case "/broken":
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            case "/slow":
                await Task.Delay(1000, token);
                return new HttpResponseMessage(HttpStatusCode.OK);
            default:
                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("<html>oops</html>") };
        }
    }
}
```

### Retries

Some failures are **temporary**: a 503, a 429, a timeout. Waiting a bit and trying again often works. But 4xx errors like 400 or 404 will not go away no matter how many times you retry.

In real apps, use the **`Microsoft.Extensions.Http.Resilience`** package (built on Polly). It adds retries, timeouts, and circuit breakers with just one line:

```text
builder.Services.AddHttpClient<ProductApiClient>(c => c.BaseAddress = new Uri("https://api.example.com/"))
    .AddStandardResilienceHandler();   // handles retries, timeouts, circuit breaker
```

Here is a simple hand-written retry to show the idea:

```csharp runnable title="Retry temporary failures"
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

var client = new HttpClient(new FlakyApi()) { BaseAddress = new Uri("https://api.example.com/") };

string result = await GetWithRetryAsync(client, "rates/inr", attempts: 3);
Console.WriteLine($"Result: {result}");

static async Task<string> GetWithRetryAsync(HttpClient client, string url, int attempts)
{
    for (int attempt = 1; ; attempt++)
    {
        HttpResponseMessage response = await client.GetAsync(url);
        bool temporary = response.StatusCode is HttpStatusCode.ServiceUnavailable or HttpStatusCode.TooManyRequests;

        if (response.IsSuccessStatusCode || !temporary || attempt == attempts)
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }

        int wait = 100 * attempt;    // wait longer each time
        Console.WriteLine($"Attempt {attempt}: {(int)response.StatusCode}, retrying in {wait} ms");
        await Task.Delay(wait);
    }
}

class FlakyApi : HttpMessageHandler
{
    private int _calls;
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken token)
    {
        _calls++;
        return Task.FromResult(_calls < 3
            ? new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
            : new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("1 USD = 83.2 INR") });
    }
}
```

### Error Handling Checklist

1. **Set a sensible timeout**, or pass a `CancellationToken` (see lesson 3).
2. **Check the status** with `IsSuccessStatusCode`, or call `EnsureSuccessStatusCode()`.
3. **Catch `HttpRequestException`**, `TaskCanceledException`, and `JsonException` where you can handle them.
4. **Only retry temporary failures** like 503 or 429. Use growing wait times, and set a limit on retries.
5. **Log failures** with the URL and status code (see lesson 23), but never log passwords or secrets.

## Where You Use HttpClient and JSON

- **Calling third-party APIs**: payment gateways, SMS and email services, maps, weather data.
- **Microservices** talking to each other inside one larger system.
- **Blazor WebAssembly apps** in the browser calling their backend server.
- **Mobile apps** made with .NET MAUI talking to a backend server.
- **Background jobs** that bring in data from other systems or sync data.

## Common Mistakes

| Mistake | What Goes Wrong | How to Fix It |
|---|---|---|
| Making a new `HttpClient()` for every request | Runs out of network connections under heavy load | Use one shared client, or `IHttpClientFactory` |
| Never checking the status code | Treats error pages as real data | Check `IsSuccessStatusCode` or call `EnsureSuccessStatusCode()` |
| `BaseAddress` without a trailing `/` | Part of the URL path gets lost | End `BaseAddress` with `/`; don't start relative paths with `/` |
| Property names don't match the JSON | Properties stay at default values | Use `PropertyNameCaseInsensitive`, camelCase policy, or `[JsonPropertyName]` |
| Sending enums as numbers | API rejects `"status": 2` | Use `JsonStringEnumConverter` |
| Catching only `HttpRequestException` | Timeouts crash the app | Also catch `TaskCanceledException` |
| Using `.Result` or `.Wait()` on HTTP calls | Program freezes or blocks threads | Use `async` and `await` throughout |
| Making new `JsonSerializerOptions` every time | Slow serialization | Reuse one static options object |

## What to Remember

- `HttpClient` sends HTTP requests. Reuse one client, or use `IHttpClientFactory`.
- GET reads data. POST creates. PUT replaces. PATCH changes fields. DELETE removes.
- Headers go on `DefaultRequestHeaders` for every request, or on one `HttpRequestMessage`.
- Check status codes: 2xx means success, 4xx means your fault, 5xx means the server's fault.
- `GetFromJsonAsync`, `PostAsJsonAsync`, and `ReadFromJsonAsync` handle JSON automatically.
- `JsonSerializer.Serialize` and `Deserialize<T>` turn objects into JSON and JSON into objects.
- `JsonSerializerOptions` changes naming, spacing, nulls, and enums. Make one and reuse it.
- Attributes like `[JsonPropertyName]`, `[JsonIgnore]`, and `[JsonRequired]` control one property.
- Wrap each API in its own typed client class.
- Expect failures: check statuses, set timeouts, catch the right exceptions, and retry only temporary failures.
