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

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

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

  • Чтение объектов CLR из текста http-сообщения
  • Запись объектов CLR в текст сообщения HTTP

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

Содержимое

Модуль форматирования json Media-Type

Форматирование 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
}

Если вы предпочитаете использовать "согласие", украсите класс атрибутом 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
}

Свойства Read-Only

Свойства только для чтения сериализуются по умолчанию.

даты.

По умолчанию Json.NET записывает даты в формате ISO 8601 . Даты в формате UTC (время в формате 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 с использованием верблюжьего регистра, не изменяя модель данных, задайте camelCasePropertyNamesContractResolver в сериализаторе:

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

Анонимные объекты и объекты Weakly-Typed

Метод действия может возвращать анонимный объект и сериализовать его в 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-Media-Type

Форматирование 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 . При наличии этого атрибута класс сериализуется следующим образом:

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

Свойства Read-Only

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

Для разных типов СРЕДЫ CLR можно задать различные xml-сериализаторы. Например, у вас может быть определенный объект данных, которому требуется 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 из списка форматировщиков, если вы не хотите их использовать. Ниже приведены main причины.

  • Чтобы ограничить ответы веб-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 не являются стандартными. Прежде чем использовать эту функцию, подумайте, смогут ли ваши клиенты анализировать результаты. Возможно, лучше просто удалить циклы из графа. Например, ссылка из Employee обратно в Отдел в этом примере не требуется.

Для сохранения ссылок на объекты в 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);
}