JSON은 대부분의 C# 프로그램이 웹 API와 대화하고, 설정을 저장하고, 데이터를 주고받는 방법입니다. 최신 .NET은 .NET Core 3.0부터 런타임의 일부인 System.Text.Json으로 JSON을 읽고 씁니다. NuGet 패키지가 필요 없습니다. 주된 진입점은 객체를 JSON 문자열로, 그리고 다시 객체로 바꾸는 정적 클래스 JsonSerializer입니다.
System.Text.Json은 .NET Core 3.0과 그 이후 모든 버전(.NET 5부터 .NET 10까지)에 내장되어 있습니다. .NET Framework 4.6.2 이상이나 .NET Standard 2.0 프로젝트에서도 System.Text.Json NuGet 패키지를 설치하면 쓸 수 있습니다. 이 페이지의 예제는 출력을 주석으로 단 일반 코드로 보여 줍니다. .NET 프로젝트에서 실행하려면 다음 using 지시문을 추가하세요:
using System.Text.Json;
using System.Text.Json.Serialization;
직렬화: 객체를 JSON으로
JsonSerializer.Serialize는 객체의 모든 public 속성을 속성 이름 그대로 씁니다:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public bool InStock { get; set; }
}
var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };
string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}
컬렉션은 배열이 되고, 문자열 키를 가진 딕셔너리는 객체가 되며({"apples":3,"pears":5}), null은 null로 남고, DateTime은 ISO 8601 문자열("2026-03-01T14:30:00")이 됩니다. 두 가지 기본 동작이 사람들을 놀라게 합니다:
- 필드는 건너뜁니다. 속성만 직렬화됩니다.
public int X;를 가진 클래스는 옵션에서IncludeFields = true를 설정하거나 필드를 속성으로 바꾸지 않으면{}로 직렬화됩니다. - 열거형은 숫자입니다.
OrderStatus.Shipped는1(열거형의 기반 값)로 쓰입니다."Shipped"로 쓰려면JsonStringEnumConverter(아래)를 추가하세요.
역직렬화: JSON을 객체로
JsonSerializer.Deserialize<T>는 T를 만들고 JSON으로부터 속성을 설정합니다:
string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}"); // Desk lamp 34.90 2
var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count); // 2
대응하는 C# 속성이 없는 JSON 속성은 무시되고, 대응하는 JSON이 없는 C# 속성은 기본값을 유지합니다. 둘 다 기본적으로 오류가 아닙니다.
거의 모든 사람이 걸리는 규칙이 있습니다. 이름 비교는 대소문자를 구분합니다. 대부분의 웹 API는 camelCase를 보내고, camelCase는 PascalCase 속성과 맞지 않습니다:
string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";
var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}"); // [] 0: nothing matched, no error
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}"); // [Mug] 8.5
new JsonSerializerOptions(JsonSerializerDefaults.Web)은 ASP.NET Core가 쓰는 설정을 줍니다. 대소문자를 구분하지 않는 읽기, camelCase 쓰기, 따옴표로 감싼 문자열로 된 숫자 허용입니다.
역직렬화에는 각 값을 설정할 방법이 필요합니다. public setter, init 접근자, 또는 매개변수 이름이 속성과 맞는 생성자입니다. 마지막 규칙 덕분에 레코드가 바로 동작합니다:
public record Point(int X, int Y);
Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt); // Point { X = 1, Y = 2 }
옵션: camelCase와 들여쓰기 출력
JsonSerializerOptions는 이름 짓기, 서식 등을 제어합니다. 인스턴스 하나를 만들어 재사용하세요. 직렬 변환기는 옵션 인스턴스마다 메타데이터를 캐시하므로, 호출마다 새 옵션을 만들면 측정할 만큼 느려집니다.
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
// "name": "Desk lamp",
// "price": 34.90,
// "tags": [
// "home",
// "light"
// ],
// "inStock": true
// }
알아 둘 만한 다른 옵션: null 속성을 빼는 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, IncludeFields = true, 문자열로 쓴 숫자를 받는 NumberHandling, 그리고 손으로 편집하는 설정 파일을 위한 ReadCommentHandling = JsonCommentHandling.Skip과 AllowTrailingCommas = true. .NET 8은 snake_case를 쓰는 API를 위해 JsonNamingPolicy.SnakeCaseLower를 추가했습니다.
특성: 이름 바꾸기, 무시하기, 문자열로 쓰는 열거형
클래스의 특성은 속성 하나씩을 제어하며 옵션보다 우선합니다:
public enum OrderStatus { Pending, Shipped }
public class Order
{
[JsonPropertyName("order_id")]
public int Id { get; set; }
public string Customer { get; set; }
[JsonIgnore]
public string InternalNote { get; set; } // never written or read
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Coupon { get; set; } // left out when null
[JsonConverter(typeof(JsonStringEnumConverter))]
public OrderStatus Status { get; set; }
public DateTime PlacedAt { get; set; }
}
var order = new Order
{
Id = 1042, Customer = "Ana", InternalNote = "vip",
Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}
속성마다 표시하는 대신 모든 열거형을 문자열로 쓰려면 옵션에 변환기를 추가하세요: options.Converters.Add(new JsonStringEnumConverter());.
클래스 없이 JSON 읽기: JsonDocument와 JsonNode
큰 응답에서 값 몇 개만 필요하거나 모양이 바뀐다면 클래스를 건너뛰세요. JsonDocument는 JsonElement 값의 읽기 전용 트리로 파싱합니다:
string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";
using (JsonDocument doc = JsonDocument.Parse(weather))
{
JsonElement root = doc.RootElement;
Console.WriteLine(root.GetProperty("city").GetString()); // Lisbon
Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5
foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
Console.WriteLine(c.GetString()); // sunny, windy
Console.WriteLine(root.TryGetProperty("humidity", out _)); // False
Console.WriteLine(root.GetProperty("alerts").ValueKind); // Null
}
GetProperty는 없는 이름에 KeyNotFoundException을 던지므로, 선택적 필드에는 TryGetProperty를 쓰세요. JsonDocument는 풀에서 메모리를 빌리므로 using으로 dispose합니다.
JSON을 수정하려면 인덱서를 가진 변경 가능한 트리를 주는 System.Text.Json.Nodes의 JsonNode(.NET 6 이상)를 쓰세요:
JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]); // Lisbon
node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}
파일과 스트림
디스크의 JSON은 파일 안의 문자열이므로, 파일 메서드가 직렬 변환기와 바로 조합됩니다:
File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.
큰 파일과 HTTP 본문에는 비동기 스트림 오버로드가 문자열 전체를 메모리에 만들지 않게 해 줍니다:
await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);
ASP.NET Core와 HttpClient에서는 직렬 변환기를 직접 호출할 일이 드뭅니다. 컨트롤러는 JSON 본문을 자동으로 바인딩하고, httpClient.GetFromJsonAsync<Order>(url)(System.Net.Http.Json)은 요청과 역직렬화를 호출 한 번으로 합니다.
오류
잘못된 JSON이나 속성 타입으로 변환할 수 없는 값은 JsonException을 던집니다. 메시지가 JSON 경로와 위치를 알려 주며, 보통 문제를 찾기에 충분합니다:
try
{
JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
Console.WriteLine(e.Message);
// The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}
Deserialize는 JSON 텍스트가 리터럴 null이면 (예외가 아니라) null을 반환하므로, 입력이 외부에서 온다면 결과를 확인하세요.
출력의 이스케이프된 문자
기본적으로 직렬 변환기는 ASCII가 아닌 문자와 HTML 안에서 안전하지 않은 문자를 이스케이프합니다:
Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}
이것은 올바른 JSON이며 원래 텍스트로 다시 읽힙니다. 출력을 HTML 페이지에 안전하게 넣을 수 있도록 이스케이프한 것입니다. 사람이 읽을 파일에는 옵션에서 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping(System.Text.Encodings.Web)을 설정하세요. "unsafe"는 결과를 HTML에 넣는 경우만 가리킵니다.
System.Text.Json과 Newtonsoft.Json
Newtonsoft.Json(Json.NET, JsonConvert 클래스)은 10년 동안 표준이었고 지금도 어디에나 있습니다. 주요 차이:
| System.Text.Json | Newtonsoft.Json | |
|---|---|---|
| 사용 가능성 | .NET Core 3.0+에 내장, .NET Framework 4.6.2+는 NuGet 패키지 | .NET Framework와 .NET용 NuGet 패키지 |
| 직렬화 / 역직렬화 | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| 이름 비교 | 기본적으로 대소문자 구분 | 대소문자 무시 |
| 타입 없는 읽기 | JsonDocument, JsonNode | JObject, JToken, JSONPath 쿼리 |
| 속성 이름 바꾸기 | [JsonPropertyName("x")] | [JsonProperty("x")] |
| 관대함 | 엄격함: 켜지 않으면 주석, 마지막 쉼표, 따옴표로 감싼 숫자 불가 | 기본적으로 관대함 |
| 성능 | 더 빠르고 할당이 적음, 트리밍과 AOT를 위한 소스 생성 | 더 느림, 리플렉션 기반 |
특성의 이름과 네임스페이스가 다르므로, 코드베이스를 바꾸는 일은 찾아 바꾸기와 더 엄격한 파싱에 대한 테스트입니다. 새 .NET 코드는 System.Text.Json으로 시작하세요.
소스 생성(.NET 6+)
JsonSerializer는 보통 실행 시점에 리플렉션으로 타입을 조사합니다. 트리밍하거나 미리 컴파일하는 앱(Native AOT, Blazor WebAssembly)에서는 소스 생성기가 대신 컴파일 시점에 그 코드를 작성합니다:
[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }
string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);
흔한 실수
- 기본 옵션으로 camelCase JSON을 PascalCase 속성에 넣기. 모든 속성이 조용히 비어 있습니다.
PropertyNameCaseInsensitive나JsonSerializerDefaults.Web을 쓰세요. - 속성 대신 public 필드.
IncludeFields를 설정하지 않으면 직렬화되지 않습니다. - setter가 없는 속성. 대응하는 생성자 매개변수가 없는 get 전용 속성은 역직렬화에서 채워지지 않습니다.
- 호출마다 새
JsonSerializerOptions. 정적 인스턴스 하나를 재사용하세요. - 부동소수점 금액.
0.1 + 0.2인double은0.30000000000000004로 직렬화됩니다. 금액에는decimal을 쓰세요.
자주 묻는 질문
C#에서 객체를 JSON으로 바꾸려면 어떻게 하나요?
System.Text.Json의 JsonSerializer.Serialize(obj)를 호출하세요. .NET Core 3.0 이상에 내장되어 있어 설치할 패키지가 없습니다. 모든 public 속성을 씁니다: {"Name":"Desk lamp","Price":34.90}. 읽기 좋은 출력에는 new JsonSerializerOptions { WriteIndented = true }를, camelCase 이름에는 PropertyNamingPolicy = JsonNamingPolicy.CamelCase를 넘기세요.
C#에서 JSON을 객체로 바꾸려면 어떻게 하나요?
var product = JsonSerializer.Deserialize<Product>(json);은 Product를 만들고 이름이 맞는 JSON 값으로 설정 가능한 public 속성을 채웁니다. 기본적으로 이름 비교는 대소문자를 구분하므로, PropertyNameCaseInsensitive = true나 new JsonSerializerOptions(JsonSerializerDefaults.Web)을 넘기지 않으면 camelCase JSON은 PascalCase 속성을 비워 둡니다. 형식이 잘못된 JSON이나 틀린 값 타입은 JsonException을 던집니다.
C#에서 클래스를 만들지 않고 JSON을 읽으려면 어떻게 하나요?
JsonDocument.Parse(json)을 쓰고 GetProperty("name"), GetString(), GetInt32(), EnumerateArray()로 RootElement를 따라가세요. 읽기 전용이고 빠르며 dispose해야 합니다. 수정하고 싶은 JSON에는 JsonNode.Parse(json)(.NET 6+)이 변경 가능한 트리를 줍니다: node["city"], 대입, ToJsonString().
System.Text.Json과 Newtonsoft.Json 중 무엇을 써야 하나요?
.NET Core 3.0 이상의 새 코드에는 System.Text.Json입니다. 내장되어 있고, 더 빠르고, 할당이 적으며, ASP.NET Core가 기본으로 씁니다. Newtonsoft.Json(Json.NET)은 .NET Framework 프로젝트(System.Text.Json을 NuGet 패키지로만 쓸 수 있는 곳), 추가 기능(JSONPath 쿼리, 아주 관대한 파싱, TypeNameHandling)에 의존하는 코드, 이미 그것으로 만든 큰 코드베이스에서 여전히 많이 쓰입니다.
System.Text.Json이 é나 < 같은 문자를 이스케이프하는 이유는 무엇인가요?
기본 인코더는 출력을 HTML에 안전하게 넣을 수 있도록 ASCII가 아닌 문자와 HTML에 민감한 문자(<, >, &, ')를 \uXXXX로 이스케이프합니다. 그래도 올바른 JSON이며 역직렬화하면 같은 텍스트로 돌아옵니다. 읽기 좋은 출력이 필요하면 옵션에서 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping을 설정하되, JSON을 HTML에 쓰지 않을 때만 그렇게 하세요.