Сериализация JSON и XML в веб-API ASP.NET

В этой статье описаны методы форматирования JSON и XML в веб-API ASP.NET.

В веб-API ASP.NET метод форматирования типа мультимедиа — это объект, который может:

  • Чтение объектов CLR из текста СООБЩЕНИЯ HTTP
  • Запись объектов CLR в текст сообщения HTTP

Веб-API предоставляет средства форматирования типов мультимедиа для JSON и XML. Платформа по умолчанию вставляет эти модули форматирования в конвейер. Клиенты могут запрашивать JSON или XML в заголовке Accept HTTP-запроса.

Содержимое

Форматировщик Media-Type JSON

Форматирование JSON предоставляется классом JsonMediaTypeFormatter . По умолчанию JsonMediaTypeFormatter использует библиотеку Json.NET для выполнения сериализации. Json.NET — это сторонний проект с открытым исходным кодом.

Если вы предпочитаете, можно настроить класс JsonMediaTypeFormatter для использования DataContractJsonSerializer вместо Json.NET. Для этого задайте для свойства UseDataContractJsonSerializerзначение true:

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

Сериализация JSON

В этом разделе описываются некоторые особенности поведения средства форматирования JSON с помощью сериализатора Json.NET по умолчанию. Это не должно быть исчерпывающей документацией по библиотеке Json.NET; Дополнительные сведения см. в документации по Json.NET.

Что сериализуется?

По умолчанию все общедоступные свойства и поля включены в сериализованный JSON. Чтобы не упустить свойство или поле, украсите его атрибутом JsonIgnore .

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

Если вы предпочитаете подход "opt-in", украсите класс атрибутом 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;

Если вы предпочитаете использовать формат даты MICROSOFT JSON ("\/Date(ticks)\/") вместо ISO 8601, задайте свойство 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 в стиле CamelCase, не изменяя модель данных, задайте для сериализатора CamelCasePropertyNamesContractResolver

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

Анонимные и слаботипизированные объекты

Метод действия может возвращать анонимный объект и сериализовать его в JSON. Рассмотрим пример.

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 не поддерживает анонимные типы или экземпляры JObject . Если эти функции используются для данных JSON, необходимо удалить xml-форматировщик из конвейера, как описано далее в этой статье.

Форматировщик XML-медиа-типа

Форматирование XML предоставляется классом XmlMediaTypeFormatter . По умолчанию XmlMediaTypeFormatter использует класс DataContractSerializer для выполнения сериализации.

Если вы предпочитаете, можно настроить XmlMediaTypeFormatter для использования XmlSerializer вместо DataContractSerializer. Для этого задайте для свойства UseXmlSerializerзначение true:

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

Класс XmlSerializer поддерживает более узкий набор типов, чем DataContractSerializer, но обеспечивает больший контроль над результирующий XML. Рекомендуется использовать XmlSerializer , если необходимо соответствовать существующей схеме XML.

Сериализация XML

В этом разделе описываются некоторые особенности поведения xml-форматирования с помощью стандартного dataContractSerializer.

По умолчанию dataContractSerializer ведет себя следующим образом:

  • Все свойства и поля, доступные для чтения и записи, сериализуются. Чтобы опустить свойство или поле, украсите его атрибутом IgnoreDataMember .
  • Закрытые и защищенные элементы не сериализуются.
  • Свойства только для чтения не сериализуются. (Однако содержимое свойства коллекции только для чтения сериализуется.)
  • Имена классов и членов записываются в XML точно так же, как они отображаются в объявлении класса.
  • Используется пространство имен XML по умолчанию.

Если вам нужен дополнительный контроль над сериализацией, можно украсить класс атрибутом DataContract . При наличии этого атрибута класс сериализуется следующим образом:

  • Подход «opt-in»: свойства и поля по умолчанию не участвуют в сериализации. Чтобы сериализовать свойство или поле, украсите его атрибутом DataMember .
  • Чтобы сериализовать приватный или защищенный элемент, отметьте его атрибутом DataMember.
  • Свойства, предназначенные только для чтения, не подвержены сериализации.
  • Чтобы изменить способ отображения имени класса в XML, задайте параметр Name в атрибуте DataContract .
  • Чтобы изменить способ отображения имени члена в XML, задайте параметр Name в атрибуте DataMember .
  • Чтобы изменить пространство имен XML, задайте параметр пространства имен в классе DataContract .

Свойства только для чтения

Свойства только для чтения не сериализуются. Если свойство только для чтения имеет резервное частное поле, можно пометить частное поле атрибутом 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 с отступами, установите свойство Indent в true:

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

Настройка сериализаторов XML для каждого типа

Вы можете установить разные сериализаторы XML для различных типов CLR. Например, у вас может быть определенный объект данных, требующий XmlSerializer для обратной совместимости. Для этого объекта можно использовать XmlSerializer и продолжать использовать DataContractSerializer для других типов.

Чтобы задать сериализатор XML для определенного типа, вызовите 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.

В следующем коде показано, как удалить форматировщики по умолчанию. Вызовите это из метода Application_Start , определенного в Global.asax.

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;
    }
}

При вызове этого действия средство форматирования сгенерирует исключение, что приведет к передаче клиенту ответа с кодом состояния 500 (внутренняя ошибка сервера).

Чтобы сохранить ссылки на объекты в JSON, добавьте следующий код в метод Application_Start в файле Global.asax:

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"}}}

Обратите внимание, что сериализатор добавляет свойство "$id" в оба объекта. Кроме того, он обнаруживает, что свойство Employee.Department создает цикл, поэтому он заменяет значение ссылкой на объект: {"$ref":"1"}.

Замечание

Ссылки на объекты не являются стандартными в ФОРМАТЕ JSON. Прежде чем использовать эту функцию, рассмотрите, смогут ли клиенты анализировать результаты. Возможно, лучше просто удалить циклы из графа. Например, ссылка от сотрудника обратно к отделу не требуется в этом примере.

Чтобы сохранить ссылки на объекты в XML, у вас есть два варианта. Проще добавить [DataContract(IsReference=true)] в класс модели. Параметр IsReference включает ссылки на объекты. Помните, что DataContract подразумевает включение в процесс сериализации, поэтому вам также потребуется добавить атрибуты 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 и установите в его конструкторе параметр preserveObjectReferences в true для сохранения ссылок на объекты. Затем задайте этот экземпляр в качестве сериализатора для типа в форматировщике медиа-типа 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);
}