ASP.NET Web API의 JSON 및 XML Serialization

이 문서에서는 ASP.NET Web API의 JSON 및 XML 포맷터에 대해 설명합니다.

ASP.NET Web API에서 미디어 형식 포맷터는 다음을 수행할 수 있는 개체입니다.

  • HTTP 메시지 본문에서 CLR 개체 읽기
  • HTTP 메시지 본문에 CLR 개체 쓰기

Web API는 JSON 및 XML 모두에 대한 미디어 형식 포맷터를 제공합니다. 프레임워크는 기본적으로 이러한 포맷터를 파이프라인에 삽입합니다. 클라이언트는 HTTP 요청의 Accept 헤더에서 JSON 또는 XML을 요청할 수 있습니다.

목차

JSON 미디어-타입 형식 지정자

JSON 형식 지정은 JsonMediaTypeFormatter 클래스에서 제공됩니다. 기본적으로 JsonMediaTypeFormatterJson.NET 라이브러리를 사용하여 serialization을 수행합니다. Json.NET 타사 오픈 소스 프로젝트입니다.

원하는 경우 Json.NET 대신 DataContractJsonSerializer를 사용하도록 JsonMediaTypeFormatter 클래스를 구성할 수 있습니다. 이렇게 하려면 UseDataContractJsonSerializer 속성을 true로 설정합니다.

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.UseDataContractJsonSerializer = true;

JSON 직렬화

이 섹션에서는 기본 Json.NET serializer를 사용하여 JSON 포맷터의 몇 가지 특정 동작에 대해 설명합니다. 이는 Json.NET 라이브러리에 대한 포괄적인 설명서가 아닙니다. 자세한 내용은 Json.NET 설명서를 참조하세요.

Serialize되는 항목

기본적으로 모든 공용 속성 및 필드는 직렬화된 JSON에 포함됩니다. 속성 또는 필드를 생략하려면 JsonIgnore 특성으로 데코레이트합니다.

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    [JsonIgnore]
    public int ProductCode { get; set; } // omitted
}

"옵트인" 방법을 선호하는 경우 DataContract 특성으로 클래스를 데코레이트합니다. 이 특성이 있는 경우 멤버는 DataMember가 없는 한 무시됩니다. DataMember를 사용하여 프라이빗 멤버를 직렬화할 수도 있습니다.

[DataContract]
public class Product
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    public int ProductCode { get; set; }  // omitted by default
}

읽기 전용 속성

읽기 전용 속성은 기본적으로 직렬화됩니다.

날짜

기본적으로 Json.NET ISO 8601 형식으로 날짜를 씁니다. UTC(협정 세계시)의 날짜는 "Z" 접미사로 작성됩니다. 현지 시간의 날짜에는 표준 시간대 오프셋이 포함됩니다. 다음은 그 예입니다.

2012-07-27T18:51:45.53403Z         // UTC
2012-07-27T11:51:45.53403-07:00    // Local

기본적으로 Json.NET 표준 시간대를 유지합니다. DateTimeZoneHandling 속성을 설정하여 이를 재정의할 수 있습니다.

// Convert all dates to UTC
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;

ISO 8601 대신 Microsoft JSON 날짜 형식 ("\/Date(ticks)\/")을 사용하려면 serializer 설정에서 DateFormatHandling 속성을 설정합니다.

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.DateFormatHandling 
= Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;

들여쓰기

들여쓰기된 JSON을 작성하려면 서식 설정을 Formatting.Indented로 설정합니다.

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

낙타표기법

데이터 모델을 변경하지 않고 JSON 속성 이름을 카멜 케이스로 작성하려면 serializer에서 CamelCasePropertyNamesContractResolver를 설정합니다.

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

익명 개체 및 약한 형식 개체

작업 메서드는 익명 개체를 반환하고 JSON으로 serialize할 수 있습니다. 다음은 그 예입니다.

public object Get()
{
    return new { 
        Name = "Alice", 
        Age = 23, 
        Pets = new List<string> { "Fido", "Polly", "Spot" } 
    };
}

응답 메시지 본문에는 다음 JSON이 포함됩니다.

{"Name":"Alice","Age":23,"Pets":["Fido","Polly","Spot"]}

웹 API가 클라이언트에서 느슨하게 구조화된 JSON 개체를 수신하는 경우 요청 본문을 Newtonsoft.Json.Linq.JObject 형식으로 역직렬화할 수 있습니다.

public void Post(JObject person)
{
    string name = person["Name"].ToString();
    int age = person["Age"].ToObject<int>();
}

그러나 일반적으로 강력한 타입의 데이터 객체를 사용하는 것이 좋습니다. 그런 다음 데이터를 직접 구문 분석할 필요가 없으며 모델 유효성 검사의 이점을 얻을 수 있습니다.

XML serializer는 익명 형식 또는 JObject 인스턴스를 지원하지 않습니다. JSON 데이터에 이러한 기능을 사용하는 경우 이 문서의 뒷부분에 설명된 대로 파이프라인에서 XML 포맷터를 제거해야 합니다.

XML 미디어 유형 형식 지정자

XML 서식은 XmlMediaTypeFormatter 클래스에서 제공합니다. 기본적으로 XmlMediaTypeFormatterDataContractSerializer 클래스를 사용하여 serialization을 수행합니다.

원하는 경우 DataContractSerializer 대신 XmlSerializer를 사용하도록 XmlMediaTypeFormatter를 구성할 수 있습니다. 이렇게 하려면 UseXmlSerializer 속성을 true로 설정합니다.

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;

XmlSerializer 클래스는 DataContractSerializer보다 더 좁은 형식 집합을 지원하지만 결과 XML을 더 많이 제어합니다. 기존 XML 스키마와 일치해야 하는 경우 XmlSerializer 를 사용하는 것이 좋습니다.

XML 직렬화

이 섹션에서는 기본 DataContractSerializer를 사용하여 XML 포맷터의 몇 가지 특정 동작에 대해 설명합니다.

기본적으로 DataContractSerializer는 다음과 같이 작동합니다.

  • 모든 공용 읽기 및 쓰기 속성과 필드가 직렬화됩니다. 속성 또는 필드를 생략하려면 IgnoreDataMember 특성으로 데코레이트합니다.
  • 프라이빗 및 보호된 멤버는 직렬화되지 않습니다.
  • 읽기 전용 속성은 serialize되지 않습니다. 그러나 읽기 전용 컬렉션 속성의 내용은 직렬화됩니다.
  • 클래스 및 멤버 이름은 클래스 선언에 나타나는 것과 똑같이 XML로 작성됩니다.
  • 기본 XML 네임스페이스가 사용됩니다.

serialization에 대한 더 많은 제어가 필요한 경우 DataContract 특성으로 클래스를 데코레이트할 수 있습니다. 이 특성이 있으면 클래스는 다음과 같이 직렬화됩니다.

  • "옵트인" 접근 방식: 속성 및 필드는 기본적으로 serialize되지 않습니다. 속성 또는 필드를 serialize하려면 DataMember 특성으로 데코레이트합니다.
  • 프라이빗 또는 보호된 멤버를 직렬화하려면 DataMember 특성으로 데코레이트합니다.
  • 읽기 전용 속성은 serialize되지 않습니다.
  • XML에 클래스 이름이 표시되는 방식을 변경하려면 DataContract 특성에서 Name 매개 변수를 설정합니다.
  • XML에 멤버 이름이 표시되는 방식을 변경하려면 DataMember 특성에서 Name 매개 변수를 설정합니다.
  • XML 네임스페이스를 변경하려면 DataContract 클래스에서 네임스페이스 매개 변수를 설정합니다.

읽기 전용 속성

읽기 전용 속성은 serialize되지 않습니다. 읽기 전용 속성에 지원 프라이빗 필드가 있는 경우 DataMember 특성으로 프라이빗 필드를 표시할 수 있습니다. 이 방법을 사용하려면 클래스의 DataContract 특성이 필요합니다.

[DataContract]
public class Product
{
    [DataMember]
    private int pcode;  // serialized

    // Not serialized (read-only)
    public int ProductCode { get { return pcode; } }
}

날짜

날짜는 ISO 8601 형식으로 작성됩니다. 예를 들어 "2012-05-23T20:21:37.9116538Z"입니다.

들여쓰기

들여쓰기된 XML을 작성하려면 들여쓰기 속성을 true로 설정합니다.

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.Indent = true;

Per-Type XML Serializer 설정

다른 CLR 형식에 대해 다른 XML 직렬 변환기를 설정할 수 있습니다. 예를 들어 이전 버전과의 호환성을 위해 XmlSerializer 가 필요한 특정 데이터 개체가 있을 수 있습니다. 이 개체에 XmlSerializer 를 사용하고 다른 형식에 DataContractSerializer 를 계속 사용할 수 있습니다.

특정 형식에 대한 XML serializer를 설정하려면 SetSerializer를 호출합니다.

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
// Use XmlSerializer for instances of type "Product":
xml.SetSerializer<Product>(new XmlSerializer(typeof(Product)));

XmlSerializer 또는 XmlObjectSerializer에서 파생되는 모든 개체를 지정할 수 있습니다.

JSON 또는 XML 포맷터 제거

사용하지 않으려면 포맷터 목록에서 JSON 포맷터 또는 XML 포맷터를 제거할 수 있습니다. 이 작업을 수행하는 주된 이유는 다음과 같습니다.

  • 웹 API 응답을 특정 미디어 형식으로 제한합니다. 예를 들어 JSON 응답만 지원하고 XML 포맷터를 제거하도록 결정할 수 있습니다.
  • 기본 포맷터를 사용자 지정 포맷터로 바꾸려면 예를 들어 JSON 포맷터를 JSON 포맷터의 사용자 지정 구현으로 바꿀 수 있습니다.

다음 코드에서는 기본 포맷터를 제거하는 방법을 보여 줍니다. Global.asax에 정의된 Application_Start 메서드에서 이 메서드를 호출합니다.

void ConfigureApi(HttpConfiguration config)
{
    // Remove the JSON formatter
    config.Formatters.Remove(config.Formatters.JsonFormatter);

    // or

    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);
}

순환 개체 참조 처리

기본적으로 JSON 및 XML 포맷터는 모든 개체를 값으로 씁니다. 두 속성이 동일한 개체를 참조하거나 같은 개체가 컬렉션에 두 번 나타나는 경우 포맷터는 개체를 두 번 직렬화합니다. 개체 그래프에 주기가 포함되어 있으면, 직렬화 도구는 그래프에서 루프를 발견할 때 예외를 발생시키기 때문에 이 문제가 특히 심각합니다.

다음 개체 모델 및 컨트롤러를 고려합니다.

public class Employee
{
    public string Name { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public string Name { get; set; }
    public Employee Manager { get; set; }
}

public class DepartmentsController : ApiController
{
    public Department Get(int id)
    {
        Department sales = new Department() { Name = "Sales" };
        Employee alice = new Employee() { Name = "Alice", Department = sales };
        sales.Manager = alice;
        return sales;
    }
}

이 작업을 호출하면 포맷터가 예외를 throw합니다. 이 예외는 클라이언트에 대한 상태 코드 500(내부 서버 오류) 응답으로 변환됩니다.

JSON에서 개체 참조를 유지하려면 Global.asax 파일의 Application_Start 메서드에 다음 코드를 추가합니다.

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.All;

이제 컨트롤러 작업은 다음과 같은 JSON을 반환합니다.

{"$id":"1","Name":"Sales","Manager":{"$id":"2","Name":"Alice","Department":{"$ref":"1"}}}

serializer는 두 개체에 "$id" 속성을 추가합니다. 또한 Employee.Department 속성이 루프를 만드는 것을 감지하여 값을 개체 참조 {"$ref":"1"}로 바꿉니다.

메모

개체 참조는 JSON에서 표준이 아닙니다. 이 기능을 사용하기 전에 클라이언트가 결과를 구문 분석할 수 있는지 여부를 고려합니다. 그래프에서 주기를 제거하는 것이 더 좋을 수 있습니다. 예를 들어 이 예제에서는 Employee에서 부서로의 링크가 실제로 필요하지 않습니다.

XML에서 개체 참조를 유지하려면 두 가지 옵션이 있습니다. 더 간단한 옵션은 모델 클래스에 추가하는 [DataContract(IsReference=true)] 것입니다. IsReference 매개 변수는 개체 참조를 사용하도록 설정합니다. DataContract는 serialization 옵트인을 수행하므로 DataMember 특성도 속성에 추가해야 합니다.

[DataContract(IsReference=true)]
public class Department
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public Employee Manager { get; set; }
}

이제 포맷터는 다음과 유사한 XML을 생성합니다.

<Department xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="i1" 
            xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" 
            xmlns="http://schemas.datacontract.org/2004/07/Models">
  <Manager>
    <Department z:Ref="i1" />
    <Name>Alice</Name>
  </Manager>
  <Name>Sales</Name>
</Department>

모델 클래스에서 특성을 방지하려면 다른 옵션이 있습니다. 즉, 새 형식별 DataContractSerializer 인스턴스를 만들고 생성자에서 preserveObjectReferencestrue 로 설정합니다. 그런 다음 XML 미디어 형식 포맷터에서 이 인스턴스를 형식별 직렬 변환기로 설정합니다. 다음 코드는 이 작업을 수행하는 방법을 보여줍니다.

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Department), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ true, null);
xml.SetSerializer<Department>(dcs);

객체 직렬화 테스트

웹 API를 디자인할 때 데이터 개체를 직렬화하는 방법을 테스트하는 것이 유용합니다. 컨트롤러를 만들거나 컨트롤러 작업을 호출하지 않고도 이 작업을 수행할 수 있습니다.

string Serialize<T>(MediaTypeFormatter formatter, T value)
{
    // Create a dummy HTTP Content.
    Stream stream = new MemoryStream();
    var content = new StreamContent(stream);
    /// Serialize the object.
    formatter.WriteToStreamAsync(typeof(T), value, stream, content, null).Wait();
    // Read the serialized string.
    stream.Position = 0;
    return content.ReadAsStringAsync().Result;
}

T Deserialize<T>(MediaTypeFormatter formatter, string str) where T : class
{
    // Write the serialized string to a memory stream.
    Stream stream = new MemoryStream();
    StreamWriter writer = new StreamWriter(stream);
    writer.Write(str);
    writer.Flush();
    stream.Position = 0;
    // Deserialize to an object of type T
    return formatter.ReadFromStreamAsync(typeof(T), stream, null, null).Result as T;
}

// Example of use
void TestSerialization()
{
    var value = new Person() { Name = "Alice", Age = 23 };

    var xml = new XmlMediaTypeFormatter();
    string str = Serialize(xml, value);

    var json = new JsonMediaTypeFormatter();
    str = Serialize(json, value);

    // Round trip
    Person person2 = Deserialize<Person>(json, str);
}