Serialização JSON e XML no ASP.NET Web API

Este artigo descreve os formatadores JSON e XML no ASP.NET Web API.

Em ASP.NET Web API, um formatador de tipo de mídia é um objeto que pode:

  • Ler objetos CLR de um corpo de mensagem HTTP
  • Gravar objetos CLR em um corpo de mensagem HTTP

A API Web fornece formatadores de tipo de mídia para JSON e XML. A estrutura insere esses formatadores no pipeline por padrão. Os clientes podem solicitar JSON ou XML no cabeçalho Accept da solicitação HTTP.

Sumário

Formatador de Media-Type JSON

A formatação JSON é fornecida pela classe JsonMediaTypeFormatter . Por padrão, JsonMediaTypeFormatter usa a biblioteca Json.NET para executar a serialização. Json.NET é um projeto de código aberto de terceiros.

Se preferir, você pode configurar a classe JsonMediaTypeFormatter para usar o DataContractJsonSerializer em vez de Json.NET. Para fazer isso, defina a propriedade UseDataContractJsonSerializer como true:

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

Serialização JSON

Esta seção descreve alguns comportamentos específicos do formatador JSON, usando o serializador de Json.NET padrão. Esta não deve ser uma documentação abrangente da biblioteca de Json.NET; para obter mais informações, consulte a Documentação do Json.NET.

O que é serializado?

Por padrão, todas as propriedades e campos públicos são incluídos no JSON serializado. Para omitir uma propriedade ou campo, decore-a com o atributo JsonIgnore .

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

Se você preferir uma abordagem de "aceitar", decore a classe com o atributo DataContract . Se esse atributo estiver presente, os membros serão ignorados, a menos que tenham o DataMember. Você também pode usar DataMember para serializar membros privados.

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

Propriedades do Read-Only

As propriedades somente leitura são serializadas por padrão.

Datas

Por padrão, Json.NET grava datas no formato ISO 8601 . As datas em UTC (Tempo Universal Coordenado) são escritas com um sufixo "Z". As datas no horário local incluem um deslocamento de fuso horário. Por exemplo:

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

Por padrão, Json.NET preserva o fuso horário. Você pode substituir isso definindo a propriedade DateTimeZoneHandling:

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

Se você preferir usar o formato de data JSON da Microsoft ("\/Date(ticks)\/") em vez da ISO 8601, defina a propriedade DateFormatHandling nas configurações do serializador:

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

Recuo

Para gravar JSON recuado, defina a configuração Formataçãocomo Formatação.Recuado:

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

Uso de maiúsculas e minúsculas de camelo

Para gravar nomes de propriedade JSON com maiúsculas e minúsculas de camel, sem alterar o modelo de dados, defina CamelCasePropertyNamesContractResolver no serializador:

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

Objetos anônimos e Weakly-Typed

Um método de ação pode retornar um objeto anônimo e serializá-lo para JSON. Por exemplo:

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

O corpo da mensagem de resposta conterá o seguinte JSON:

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

Se a API Web receber objetos JSON livremente estruturados de clientes, você poderá desserializar o corpo da solicitação para um tipo Newtonsoft.Json.Linq.JObject .

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

No entanto, geralmente é melhor usar objetos de dados fortemente tipado. Em seguida, você não precisa analisar os dados por conta própria e obter os benefícios da validação do modelo.

O serializador XML não dá suporte a tipos anônimos ou instâncias JObject . Se você usar esses recursos para seus dados JSON, deverá remover o formatador XML do pipeline, conforme descrito mais adiante neste artigo.

Formatador de Media-Type XML

A formatação XML é fornecida pela classe XmlMediaTypeFormatter . Por padrão, XmlMediaTypeFormatter usa a classe DataContractSerializer para executar a serialização.

Se preferir, você pode configurar o XmlMediaTypeFormatter para usar o XmlSerializer em vez do DataContractSerializer. Para fazer isso, defina a propriedade UseXmlSerializer como true:

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

A classe XmlSerializer dá suporte a um conjunto mais estreito de tipos do que DataContractSerializer, mas fornece mais controle sobre o XML resultante. Considere o uso de XmlSerializer se você precisar corresponder a um esquema XML existente.

Serialização XML

Esta seção descreve alguns comportamentos específicos do formatador XML, usando o DataContractSerializer padrão.

Por padrão, o DataContractSerializer se comporta da seguinte maneira:

  • Todas as propriedades e campos públicos de leitura/gravação são serializados. Para omitir uma propriedade ou campo, decore-a com o atributo IgnoreDataMember .
  • Membros privados e protegidos não são serializados.
  • As propriedades somente leitura não são serializadas. (No entanto, o conteúdo de uma propriedade de coleção somente leitura é serializado.)
  • Os nomes de classe e membro são escritos no XML exatamente como aparecem na declaração de classe.
  • Um namespace XML padrão é usado.

Se você precisar de mais controle sobre a serialização, poderá decorar a classe com o atributo DataContract . Quando esse atributo está presente, a classe é serializada da seguinte maneira:

  • Abordagem "Aceitar": propriedades e campos não são serializados por padrão. Para serializar uma propriedade ou campo, decore-a com o atributo DataMember .
  • Para serializar um membro privado ou protegido, decore-o com o atributo DataMember .
  • As propriedades somente leitura não são serializadas.
  • Para alterar como o nome da classe aparece no XML, defina o parâmetro Name no atributo DataContract .
  • Para alterar como um nome de membro aparece no XML, defina o parâmetro Name no atributo DataMember .
  • Para alterar o namespace XML, defina o parâmetro Namespace na classe DataContract .

Propriedades do Read-Only

As propriedades somente leitura não são serializadas. Se uma propriedade somente leitura tiver um campo privado de backup, você poderá marcar o campo privado com o atributo DataMember . Essa abordagem requer o atributo DataContract na classe .

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

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

Datas

As datas são escritas no formato ISO 8601. Por exemplo, "2012-05-23T20:21:37.9116538Z".

Recuo

Para gravar XML recuado, defina a propriedade Recuo como true:

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

Configurando Per-Type serializadores XML

Você pode definir serializadores XML diferentes para diferentes tipos CLR. Por exemplo, você pode ter um objeto de dados específico que requer XmlSerializer para compatibilidade com versões anteriores. Você pode usar XmlSerializer para esse objeto e continuar a usar DataContractSerializer para outros tipos.

Para definir um serializador XML para um tipo específico, chame SetSerializer.

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

Você pode especificar um XmlSerializer ou qualquer objeto derivado de XmlObjectSerializer.

Removendo o formatador JSON ou XML

Você pode remover o formatador JSON ou o formatador XML da lista de formatadores, se não quiser usá-los. As main razões para fazer isso são:

  • Para restringir as respostas da API Web a um tipo de mídia específico. Por exemplo, você pode decidir dar suporte apenas a respostas JSON e remover o formatador XML.
  • Para substituir o formatador padrão por um formatador personalizado. Por exemplo, você pode substituir o formatador JSON por sua própria implementação personalizada de um formatador JSON.

O código a seguir mostra como remover os formatadores padrão. Chame isso do método Application_Start , definido em 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);
}

Tratamento de referências de objeto circular

Por padrão, os formatadores JSON e XML gravam todos os objetos como valores. Se duas propriedades se referirem ao mesmo objeto ou se o mesmo objeto aparecer duas vezes em uma coleção, o formatador serializará o objeto duas vezes. Esse é um problema específico se o grafo de objeto contiver ciclos, pois o serializador gerará uma exceção quando detectar um loop no grafo.

Considere os seguintes modelos de objeto e controlador.

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

Invocar essa ação fará com que o formatador gere uma exceção, que se traduz em uma resposta do código 500 de status (Erro interno do servidor) ao cliente.

Para preservar referências de objeto em JSON, adicione o seguinte código ao método Application_Start no arquivo Global.asax:

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

Agora, a ação do controlador retornará JSON semelhante a este:

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

Observe que o serializador adiciona uma propriedade "$id" a ambos os objetos. Além disso, ele detecta que a propriedade Employee.Department cria um loop, portanto, substitui o valor por uma referência de objeto: {"$ref":"1"}.

Observação

As referências de objeto não são padrão no JSON. Antes de usar esse recurso, considere se seus clientes poderão analisar os resultados. Talvez seja melhor simplesmente remover ciclos do grafo. Por exemplo, o link de Funcionário de volta ao Departamento não é realmente necessário neste exemplo.

Para preservar referências de objeto em XML, você tem duas opções. A opção mais simples é adicionar [DataContract(IsReference=true)] à classe de modelo. O parâmetro IsReference habilita referências de objeto. Lembre-se de que o DataContract faz a aceitação de serialização, portanto, você também precisará adicionar atributos DataMember às propriedades:

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

Agora, o formatador produzirá XML semelhante ao seguinte:

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

Se você quiser evitar atributos em sua classe de modelo, há outra opção: Criar uma nova instância de DataContractSerializer específica do tipo e definir preserveObjectReferences como true no construtor. Em seguida, defina essa instância como um serializador por tipo no formatador de tipo de mídia XML. O código a seguir mostra como fazer isso:

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

Testando a serialização de objetos

Ao criar sua API Web, é útil testar como seus objetos de dados serão serializados. Você pode fazer isso sem criar um controlador ou invocar uma ação do controlador.

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