Serialización JSON y XML en ASP.NET Web API

En este artículo se describen los formateadores JSON y XML en ASP.NET Web API.

En ASP.NET Web API, un formateador de tipo multimedia es un objeto que puede:

  • Leer objetos CLR desde un cuerpo del mensaje HTTP
  • Escribir objetos CLR en un cuerpo del mensaje HTTP

La Web API proporciona formateadores de tipo multimedia para JSON y XML. El marco inserta estos formateadores en la canalización de forma predeterminada. Los clientes pueden solicitar JSON o XML en el Accept del encabezado de la solicitud HTTP.

Contenido

Formateador de tipo multimedia JSON

El formato JSON es provisto por la clase JsonMediaTypeFormatter. De forma predeterminada, JsonMediaTypeFormatter usa la biblioteca Json.NET para realizar la serialización. Json.NET es un proyecto de código abierto de terceros.

Si lo prefiere, puede configurar la clase JsonMediaTypeFormatter para usar DataContractJsonSerializer en lugar de Json.NET. Para ello, establezca la propiedad UseDataContractJsonSerializer en true:

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

Serialización de JSON

En esta sección se describen algunos comportamientos específicos del formateador JSON mientras utiliza el serializador Json.NET predeterminado. Esto no está pensado para ser una documentación completa de la biblioteca de Json.NET; para obtener más información, consulte la documentación de Json.NET.

¿Qué se serializa?

De forma predeterminada, todas las propiedades y campos públicos se incluyen en el JSON serializado. Para omitir una propiedad o un campo, decorar con el atributo JsonIgnore.

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

Si prefiere un enfoque de "participación", decorar la clase con el atributo DataContract. Si este atributo está presente, los miembros se omiten a menos que tengan DataMember. También puede usar DataMember para serializar miembros 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
}

Propiedades de solo lectura

Las propiedades de solo lectura se serializan de forma predeterminada.

Fechas

De forma predeterminada, Json.NET escribe fechas en formato ISO 8601. Las fechas en UTC (hora universal coordinada) se escriben con un sufijo "Z". Las fechas de la hora local incluyen un desplazamiento de zona horaria. Por ejemplo:

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

De forma predeterminada, Json.NET conserva la zona horaria. Puede invalidar esto estableciendo la propiedad DateTimeZoneHandling:

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

Si prefiere usar el formato de fecha de Microsoft JSON ("\/Date(ticks)\/") en lugar de ISO 8601, establezca la propiedad DateFormatHandling en la configuración del serializador:

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

Sangrías

Para escribir JSON con sangría, establezca la configuración de Formato en Format.Indented:

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

Grafía Camel

Para escribir nombres de propiedad JSON con grafía camel, sin cambiar el modelo de datos, establezca CamelCasePropertyNamesContractResolver en el serializador:

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

Objetos anónimos y con tipeado débil

Un método de acción puede devolver un objeto anónimo y serializarlo en JSON. Por ejemplo:

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

El cuerpo del mensaje de respuesta contendrá el siguiente JSON:

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

Si la API web recibe objetos JSON estructurados de forma flexible de los clientes, puede deserializar el cuerpo de la solicitud en un tipo Newtonsoft.Json.Linq.JObject.

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

Sin embargo, normalmente es mejor usar objetos de datos fuertemente tipeados. Entonces, no es necesario analizar los datos y obtendrá las ventajas de la validación del modelo.

El serializador XML no admite tipos anónimos ni instancias de JObject. Si usa estas características para los datos JSON, debe quitar el formateador XML de la canalización, como se describe más adelante en este artículo.

Formateador de tipo multimedia.XML

El formato XML es proporcionado por la clase XmlMediaTypeFormatter. De forma predeterminada, XmlMediaTypeFormatter usa la clase DataContractSerializer para realizar la serialización.

Si lo prefiere, puede configurar XmlMediaTypeFormatter para usar XmlSerializer en lugar de DataContractSerializer. Para ello, establezca la propiedad UseXmlSerializer en true:

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

La clase XmlSerializer admite un conjunto más estrecho de tipos que DataContractSerializer, pero proporciona más control sobre el XML resultante. Considere la posibilidad de usar XmlSerializer si necesita coincidir con un esquema XML existente.

Serialización XML

En esta sección se describen algunos comportamientos específicos del formateador XML que utiliza el DataContractSerializer predeterminado.

De forma predeterminada, DataContractSerializer se comporta de la siguiente manera:

  • Se serializan todas las propiedades y campos públicos de lectura y escritura. Para omitir una propiedad o un campo, decorar con el atributo IgnoreDataMember.
  • Los miembros privados y protegidos no se serializan.
  • Las propiedades de solo lectura no se serializan. (Sin embargo, el contenido de una propiedad de colección de solo lectura se serializa).
  • Los nombres de clase y miembro se escriben en el XML exactamente como aparecen en la declaración de clase.
  • Se usa un espacio de nombres XML predeterminado.

Si necesita más control sobre la serialización, puede decorar la clase con el atributo DataContract. Cuando este atributo está presente, la clase se serializa de la siguiente manera:

  • Enfoque de "participación": las propiedades y los campos no se serializan de forma predeterminada. Para serializar una propiedad o un campo, decorar con el atributoDataMember.
  • Para serializar un miembro privado o protegido, decora con el atributo DataMember.
  • Las propiedades de solo lectura no se serializan.
  • Para cambiar cómo aparece el nombre de clase en el XML, establezca el parámetro Name en el atributo DataContract.
  • Para cambiar cómo aparece un nombre de miembro en el XML, establezca el parámetro Name en el atributo DataMember.
  • Para cambiar el espacio de nombres XML, establezca el parámetro Namespace en la clase DataContract.

Propiedades de solo lectura

Las propiedades de solo lectura no se serializan. Si una propiedad de solo lectura tiene un campo privado de respaldo, puede marcar el campo privado con el atributo DataMember. Este enfoque requiere el atributo DataContract en la clase.

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

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

Fechas

Las fechas se escriben en formato ISO 8601. Por ejemplo, "2012-05-23T20:21:37.9116538Z".

Sangrías

Para escribir XML con sangría, establezca la propiedad Sangría en true:

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

Establecer serializadores XML por tipo

Puede establecer diferentes serializadores XML para diferentes tipos CLR. Por ejemplo, puede tener un objeto de datos determinado que requiera XmlSerializer para la compatibilidad con versiones anteriores. Puede usar XmlSerializer para este objeto y seguir usando DataContractSerializer para otros tipos.

Para establecer un serializador XML para un tipo determinado, llame a SetSerializer.

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

Puede especificar un XmlSerializer o cualquier objeto que derive de XmlObjectSerializer.

Quitar el formateador JSON o XML

Puede quitar el formateador JSON o el XML de la lista de formateadores, si no desea usarlos. Las principales razones para hacerlo son:

  • Para restringir las respuestas de la API web a un tipo de medio determinado. Por ejemplo, puede decidir admitir solo respuestas JSON y quitar el formateador XML.
  • Para reemplazar el formateador predeterminado por un formateador personalizado. Por ejemplo, podría reemplazar el formateador JSON por su propia implementación personalizada de un formateador JSON.

En el código siguiente se muestra cómo quitar los formateadores predeterminados. Llame a esto desde el método Application_Start, definido en 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);
}

Controlar las referencias de objetos circulares

De forma predeterminada, los formateadores JSON y XML escriben todos los objetos como valores. Si dos propiedades hacen referencia al mismo objeto o si el mismo objeto aparece dos veces en una colección, el formateador serializará el objeto dos veces. Se trata de un problema determinado si el gráfico de objetos contiene ciclos, ya que el serializador producirá una excepción cuando detecte un bucle en el gráfico.

Tenga en cuenta los siguientes modelos de objetos y 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 esta acción hará que el formateador produzca una excepción, lo que se traduce en una respuesta de código de estado 500 (Error interno del servidor) al cliente.

Para conservar las referencias de objeto en JSON, agregue el código siguiente al método Application_Start en el archivo Global.asax:

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

Ahora, la acción del controlador devolverá JSON similar a la siguiente:

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

Observe que el serializador agrega una propiedad "$id" a ambos objetos. Además, detecta que la propiedad Employee.Department crea un bucle, por lo que reemplaza el valor por una referencia de objeto: {"$ref":"1"}.

Nota:

Las referencias de objeto no son estándar en JSON. Antes de usar esta característica, considere si los clientes podrán analizar los resultados. Puede ser mejor quitar ciclos del grafo. Por ejemplo, el vínculo desde Empleado hacia Departamento no es realmente necesario en este ejemplo.

Para conservar las referencias de objeto en XML, tiene dos opciones. La opción más sencilla es agregar [DataContract(IsReference=true)] a la clase de modelo. El parámetro IsReference habilita las referencias de objeto. Recuerde que DataContract inicia la participación en la serialización, por lo que también tendrá que agregar atributos DataMember a las propiedades:

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

Ahora, el formateador generará XML similar al siguiente:

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

Si desea evitar atributos en la clase de modelo, hay otra opción: Crear una nueva instancia específica del tipo DataContractSerializer y establecer preserveObjectReferences en true en el constructor. A continuación, establezca esta instancia como un serializador por tipo en el formateador de tipo multimedia XML. El código siguiente muestra cómo hacerlo:

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

Prueba de serialización de objetos

A medida que diseña la API web, resulta útil probar cómo se serializarán los objetos de datos. Puede hacerlo sin crear un controlador ni invocar una acción de 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);
}