Udostępnij za pośrednictwem


Serializacja JSON i XML w internetowym interfejsie API ASP.NET

W tym artykule opisano formatery JSON i XML w interfejsie API sieci Web ASP.NET.

W ASP.NET internetowym interfejsem API formater typu nośnika jest obiektem, który może:

  • Odczytywanie obiektów CLR z treści komunikatu HTTP
  • Zapisywanie obiektów CLR w treści komunikatu HTTP

Internetowy interfejs API udostępnia formatery typów multimediów zarówno dla formatu JSON, jak i XML. Struktura domyślnie wstawia te formatery do potoku. Klienci mogą zażądać kodu JSON lub XML w nagłówku Accept żądania HTTP.

Zawartość

Formater Media-Type JSON

Formatowanie JSON jest dostarczane przez klasę JsonMediaTypeFormatter . Domyślnie program JsonMediaTypeFormatter używa biblioteki Json.NET do wykonywania serializacji. Json.NET to projekt open source innej firmy.

Jeśli wolisz, możesz skonfigurować klasę JsonMediaTypeFormatter , aby zamiast Json.NET użyć klasy DataContractJsonSerializer . W tym celu ustaw właściwość UseDataContractJsonSerializer na true:

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

Serializacja JSON

W tej sekcji opisano niektóre konkretne zachowania formatatora JSON przy użyciu domyślnego serializatora Json.NET . Nie jest to kompleksowa dokumentacja biblioteki Json.NET; Aby uzyskać więcej informacji, zobacz dokumentację Json.NET.

Co jest serializowane?

Domyślnie wszystkie publiczne właściwości i pola są uwzględniane w serializowanym formacie JSON. Aby pominąć właściwość lub pole, ozdobić go atrybutem JsonIgnore .

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

Jeśli wolisz podejście "opt-in", dekoruj klasę za pomocą atrybutu DataContract . Jeśli ten atrybut jest obecny, elementy członkowskie są ignorowane, chyba że mają element DataMember. Można również użyć elementu DataMember , aby serializować prywatne elementy członkowskie.

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

właściwości Read-Only

Właściwości tylko do odczytu są domyślnie serializowane.

Daty

Domyślnie Json.NET zapisuje daty w formacie ISO 8601 . Daty w formacie UTC (uniwersalny czas koordynowany) są zapisywane z sufiksem "Z". Daty w czasie lokalnym obejmują przesunięcie strefy czasowej. Na przykład:

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

Domyślnie Json.NET zachowuje strefę czasową. Można to przesłonić, ustawiając właściwość DateTimeZoneHandling:

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

Jeśli wolisz używać formatu daty JSON firmy Microsoft ("\/Date(ticks)\/") zamiast ISO 8601, ustaw właściwość DateFormatHandling w ustawieniach serializatora:

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

Wcięcia

Aby zapisać wcięcie JSON, ustaw ustawienie Formatowanie naFormatting.Indented:

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

Camel Casing

Aby zapisać nazwy właściwości JSON z wielkością wielbłąda, bez zmieniania modelu danych ustaw właściwość CamelCasePropertyNamesContractResolver na serializatorze:

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

Obiekty anonimowe i Weakly-Typed

Metoda akcji może zwrócić anonimowy obiekt i serializować go do formatu JSON. Na przykład:

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

Treść komunikatu odpowiedzi będzie zawierać następujący kod JSON:

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

Jeśli internetowy interfejs API odbiera luźno ustrukturyzowane obiekty JSON od klientów, można deserializować treść żądania do typu Newtonsoft.Json.Linq.JObject .

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

Jednak zazwyczaj lepiej jest używać silnie typiowanych obiektów danych. Następnie nie musisz analizować danych samodzielnie i uzyskać korzyści z weryfikacji modelu.

Serializator XML nie obsługuje typów anonimowych ani wystąpień JObject . Jeśli używasz tych funkcji dla danych JSON, usuń formater XML z potoku, zgodnie z opisem w dalszej części tego artykułu.

XML Media-Type Formatter

Formatowanie XML jest dostarczane przez klasę XmlMediaTypeFormatter . Domyślnie klasa XmlMediaTypeFormatter używa klasy DataContractSerializer do wykonywania serializacji.

Jeśli wolisz, możesz skonfigurować element XmlMediaTypeFormatter tak, aby używał klasy XmlSerializer zamiast elementu DataContractSerializer. W tym celu ustaw właściwość UseXmlSerializer na true:

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

Klasa XmlSerializer obsługuje węższy zestaw typów niż DataContractSerializer, ale zapewnia większą kontrolę nad wynikowym kodem XML. Rozważ użycie elementu XmlSerializer , jeśli musisz dopasować istniejący schemat XML.

Serializacji XML

W tej sekcji opisano niektóre konkretne zachowania formatującego XML przy użyciu domyślnego elementu DataContractSerializer.

Domyślnie element DataContractSerializer zachowuje się w następujący sposób:

  • Wszystkie publiczne właściwości i pola odczytu/zapisu są serializowane. Aby pominąć właściwość lub pole, udekoruj ją atrybutem IgnoreDataMember .
  • Prywatne i chronione elementy członkowskie nie są serializowane.
  • Właściwości tylko do odczytu nie są serializowane. (Jednak zawartość właściwości kolekcji tylko do odczytu jest serializowana).
  • Nazwy klas i składowych są zapisywane w kodzie XML dokładnie tak, jak są wyświetlane w deklaracji klasy.
  • Używana jest domyślna przestrzeń nazw XML.

Jeśli potrzebujesz większej kontroli nad serializacji, możesz ozdobić klasę atrybutem DataContract . Gdy ten atrybut jest obecny, klasa jest serializowana w następujący sposób:

  • Podejście "Opt in": Właściwości i pola nie są domyślnie serializowane. Aby serializować właściwość lub pole, udekoruj ją atrybutem DataMember .
  • Aby serializować prywatny lub chroniony element członkowski, należy go ozdobić atrybutem DataMember .
  • Właściwości tylko do odczytu nie są serializowane.
  • Aby zmienić sposób wyświetlania nazwy klasy w pliku XML, ustaw parametr Name w atrybucie DataContract .
  • Aby zmienić sposób wyświetlania nazwy elementu członkowskiego w pliku XML, ustaw parametr Name w atrybucie DataMember .
  • Aby zmienić przestrzeń nazw XML, ustaw parametr przestrzeń nazw w klasie DataContract .

właściwości Read-Only

Właściwości tylko do odczytu nie są serializowane. Jeśli właściwość tylko do odczytu ma pole prywatne z kopią zapasową, możesz oznaczyć pole prywatne atrybutem DataMember . Takie podejście wymaga atrybutu DataContract w klasie .

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

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

Daty

Daty są zapisywane w formacie ISO 8601. Na przykład "2012-05-23T20:21:37.9116538Z".

Wcięcia

Aby zapisać wcięcie XML, ustaw właściwość Indent na true:

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

Ustawianie Per-Type serializatorów XML

Można ustawić różne serializatory XML dla różnych typów CLR. Na przykład może istnieć określony obiekt danych, który wymaga elementu XmlSerializer w celu zapewnienia zgodności z poprzednimi wersjami. Możesz użyć elementu XmlSerializer dla tego obiektu i nadal używać elementu DataContractSerializer dla innych typów.

Aby ustawić serializator XML dla określonego typu, wywołaj metodę SetSerializer.

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

Można określić element XmlSerializer lub dowolny obiekt pochodzący z elementu XmlObjectSerializer.

Usuwanie formatu JSON lub XML

Jeśli nie chcesz ich używać, możesz usunąć z listy formaterów JSON lub formater XML. Główne powody, dla których należy to zrobić, to:

  • Aby ograniczyć odpowiedzi internetowego interfejsu API do określonego typu nośnika. Na przykład możesz zdecydować się na obsługę tylko odpowiedzi JSON i usunąć formater XML.
  • Aby zastąpić domyślny formater niestandardowym. Możesz na przykład zastąpić formater JSON własną niestandardową implementacją programu formatującego JSON.

Poniższy kod pokazuje, jak usunąć domyślne formatery. Wywołaj to z metody Application_Start zdefiniowanej w pliku 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);
}

Obsługa odwołań do obiektu cyklicznego

Domyślnie formatery JSON i XML zapisują wszystkie obiekty jako wartości. Jeśli dwie właściwości odwołują się do tego samego obiektu lub jeśli ten sam obiekt pojawia się dwa razy w kolekcji, formater będzie serializować obiekt dwa razy. Jest to konkretny problem, jeśli wykres obiektu zawiera cykle, ponieważ serializator zgłosi wyjątek podczas wykrywania pętli na grafie.

Rozważ następujące modele obiektów i kontroler.

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

Wywołanie tej akcji spowoduje zgłoszenie wyjątku przez program formatujący, co przekłada się na odpowiedź klienta z kodem stanu 500 (wewnętrzny błąd serwera).

Aby zachować odwołania do obiektów w formacie JSON, dodaj następujący kod, aby Application_Start metodę w pliku Global.asax:

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

Teraz akcja kontrolera zwróci kod JSON, który wygląda następująco:

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

Zwróć uwagę, że serializator dodaje właściwość "$id" do obu obiektów. Ponadto wykrywa, że właściwość Employee.Department tworzy pętlę, więc zastępuje wartość odwołaniem do obiektu: {"$ref":"1"}.

Uwaga

Odwołania do obiektów nie są standardowe w formacie JSON. Przed użyciem tej funkcji należy rozważyć, czy klienci będą mogli analizować wyniki. Lepszym rozwiązaniem może być usunięcie cykli z grafu. Na przykład link od pracownika z powrotem do działu nie jest naprawdę potrzebny w tym przykładzie.

Aby zachować odwołania do obiektów w kodzie XML, dostępne są dwie opcje. Prostszą opcją jest dodanie [DataContract(IsReference=true)] do klasy modelu. Parametr IsReference umożliwia odwołania do obiektów. Pamiętaj, że funkcja DataContract wyraża zgodę na serializacji, dlatego należy również dodać atrybuty DataMember do właściwości:

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

Teraz program formatujący utworzy kod XML podobny do następującego:

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

Jeśli chcesz uniknąć atrybutów w klasie modelu, istnieje inna opcja: Utwórz nowe wystąpienie klasy DataContractSerializer specyficzne dla typu i ustaw parametr preserveObjectReferences na wartość true w konstruktorze. Następnie ustaw to wystąpienie jako serializator dla poszczególnych typów w formacie nośnika XML. Poniższy kod pokazuje, jak to zrobić:

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

Testowanie serializacji obiektów

Podczas projektowania internetowego interfejsu API warto przetestować sposób serializacji obiektów danych. Można to zrobić bez tworzenia kontrolera lub wywoływania akcji kontrolera.

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