Procedimiento para usar un documento JSON, Utf8JsonReader y Utf8JsonWriter en System.Text.Json

En este artículo se muestra cómo usar:

Elecciones de DOM JSON

El trabajo con un DOM es una alternativa a la deserialización con JsonSerializer:

  • Si no se tiene un tipo en el que deserializar.
  • Si el JSON que se recibe no tiene un esquema fijo y hay que inspeccionarlo para saber lo que contiene.

System.Text.Json proporciona dos maneras de compilar un DOM JSON:

  • JsonDocument ofrece la posibilidad de compilar un DOM de solo lectura mediante Utf8JsonReader. A los elementos JSON que componen la carga se puede acceder mediante el tipo JsonElement. El tipo JsonElement contiene los enumeradores de matriz y objeto junto con las API para convertir texto JSON en tipos de .NET comunes. JsonDocument expone una propiedad RootElement. Para obtener más información, vea Uso de JsonDocument más adelante en este artículo.

Tenga en cuenta los siguientes factores a la hora de elegir entre JsonDocument y JsonNode:

  • El DOM JsonNode se puede cambiar después de su creación. El DOM JsonDocument es inmutable.
  • El DOM JsonDocument proporciona un acceso más rápido a sus datos.

Use JsonNode

En el ejemplo siguiente se muestra cómo usar JsonNode y los otros tipos del espacio de nombres System.Text.Json.Nodes para:

  • Crear un DOM a partir de una cadena JSON
  • Escribir JSON desde un DOM.
  • Obtener un valor, un objeto o una matriz de un DOM.
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromStringExample;

public class Program
{
    public static void Main()
    {
        string jsonString =
@"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
        // Create a JsonNode DOM from a JSON string.
        JsonNode forecastNode = JsonNode.Parse(jsonString)!;

        // Write JSON from a JsonNode
        var options = new JsonSerializerOptions { WriteIndented = true };
        Console.WriteLine(forecastNode!.ToJsonString(options));
        // output:
        //{
        //  "Date": "2019-08-01T00:00:00",
        //  "Temperature": 25,
        //  "Summary": "Hot",
        //  "DatesAvailable": [
        //    "2019-08-01T00:00:00",
        //    "2019-08-02T00:00:00"
        //  ],
        //  "TemperatureRanges": {
        //    "Cold": {
        //      "High": 20,
        //      "Low": -10
        //    },
        //    "Hot": {
        //      "High": 60,
        //      "Low": 20
        //    }
        //  }
        //}

        // Get value from a JsonNode.
        JsonNode temperatureNode = forecastNode!["Temperature"]!;
        Console.WriteLine($"Type={temperatureNode.GetType()}");
        Console.WriteLine($"JSON={temperatureNode.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
        //JSON = 25

        // Get a typed value from a JsonNode.
        int temperatureInt = (int)forecastNode!["Temperature"]!;
        Console.WriteLine($"Value={temperatureInt}");
        //output:
        //Value=25

        // Get a typed value from a JsonNode by using GetValue<T>.
        temperatureInt = forecastNode!["Temperature"]!.GetValue<int>();
        Console.WriteLine($"TemperatureInt={temperatureInt}");
        //output:
        //Value=25

        // Get a JSON object from a JsonNode.
        JsonNode temperatureRanges = forecastNode!["TemperatureRanges"]!;
        Console.WriteLine($"Type={temperatureRanges.GetType()}");
        Console.WriteLine($"JSON={temperatureRanges.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonObject
        //JSON = { "Cold":{ "High":20,"Low":-10},"Hot":{ "High":60,"Low":20} }

        // Get a JSON array from a JsonNode.
        JsonNode datesAvailable = forecastNode!["DatesAvailable"]!;
        Console.WriteLine($"Type={datesAvailable.GetType()}");
        Console.WriteLine($"JSON={datesAvailable.ToJsonString()}");
        //output:
        //datesAvailable Type = System.Text.Json.Nodes.JsonArray
        //datesAvailable JSON =["2019-08-01T00:00:00", "2019-08-02T00:00:00"]

        // Get an array element value from a JsonArray.
        JsonNode firstDateAvailable = datesAvailable[0]!;
        Console.WriteLine($"Type={firstDateAvailable.GetType()}");
        Console.WriteLine($"JSON={firstDateAvailable.ToJsonString()}");
        //output:
        //Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
        //JSON = "2019-08-01T00:00:00"

        // Get a typed value by chaining references.
        int coldHighTemperature = (int)forecastNode["TemperatureRanges"]!["Cold"]!["High"]!;
        Console.WriteLine($"TemperatureRanges.Cold.High={coldHighTemperature}");
        //output:
        //TemperatureRanges.Cold.High = 20

        // Parse a JSON array
        var datesNode = JsonNode.Parse(@"[""2019-08-01T00:00:00"",""2019-08-02T00:00:00""]");
        JsonNode firstDate = datesNode![0]!.GetValue<DateTime>();
        Console.WriteLine($"firstDate={ firstDate}");
        //output:
        //firstDate = "2019-08-01T00:00:00"
    }
}

Creación de un DOM JsonNode con inicializadores de objeto y realización de cambios

El ejemplo siguiente muestra cómo:

  • Crear un DOM mediante inicializadores de objeto.
  • Realizar cambios en un DOM.
using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodeFromObjectExample;

public class Program
{
    public static void Main()
    {
        // Create a new JsonObject using object initializers.
        var forecastObject = new JsonObject
        {
            ["Date"] = new DateTime(2019, 8, 1),
            ["Temperature"] = 25,
            ["Summary"] = "Hot",
            ["DatesAvailable"] = new JsonArray(
                new DateTime(2019, 8, 1), new DateTime(2019, 8, 2)),
            ["TemperatureRanges"] = new JsonObject
            {
                ["Cold"] = new JsonObject
                {
                    ["High"] = 20,
                    ["Low"] = -10
                }
            },
            ["SummaryWords"] = new JsonArray("Cool", "Windy", "Humid")
        };

        // Add an object.
        forecastObject!["TemperatureRanges"]!["Hot"] =
            new JsonObject { ["High"] = 60, ["Low"] = 20 };

        // Remove a property.
        forecastObject.Remove("SummaryWords");

        // Change the value of a property.
        forecastObject["Date"] = new DateTime(2019, 8, 3);

        var options = new JsonSerializerOptions { WriteIndented = true };
        Console.WriteLine(forecastObject.ToJsonString(options));
        //output:
        //{
        //  "Date": "2019-08-03T00:00:00",
        //  "Temperature": 25,
        //  "Summary": "Hot",
        //  "DatesAvailable": [
        //    "2019-08-01T00:00:00",
        //    "2019-08-02T00:00:00"
        //  ],
        //  "TemperatureRanges": {
        //    "Cold": {
        //      "High": 20,
        //      "Low": -10
        //    },
        //    "Hot": {
        //      "High": 60,
        //      "Low": 20
        //    }
        //  }
        //}
    }
}

Deserialización de subsecciones de una carga JSON

En el ejemplo siguiente se muestra cómo usar JsonNode para ir a una subsección de un árbol JSON y deserializar un único valor, un tipo personalizado o una matriz de esa subsección.

using System.Text.Json;
using System.Text.Json.Nodes;

namespace JsonNodePOCOExample;

public class TemperatureRanges : Dictionary<string, HighLowTemps>
{
}

public class HighLowTemps
{
    public int High { get; set; }
    public int Low { get; set; }
}

public class Program
{
    public static DateTime[]? DatesAvailable { get; set; }

    public static void Main()
    {
        string jsonString =
@"{
  ""Date"": ""2019-08-01T00:00:00"",
  ""Temperature"": 25,
  ""Summary"": ""Hot"",
  ""DatesAvailable"": [
    ""2019-08-01T00:00:00"",
    ""2019-08-02T00:00:00""
  ],
  ""TemperatureRanges"": {
      ""Cold"": {
          ""High"": 20,
          ""Low"": -10
      },
      ""Hot"": {
          ""High"": 60,
          ""Low"": 20
      }
  }
}
";
        // Parse all of the JSON.
        JsonNode forecastNode = JsonNode.Parse(jsonString)!;

        // Get a single value
        int hotHigh = forecastNode["TemperatureRanges"]!["Hot"]!["High"]!.GetValue<int>();
        Console.WriteLine($"Hot.High={hotHigh}");
        // output:
        //Hot.High=60

        // Get a subsection and deserialize it into a custom type.
        JsonObject temperatureRangesObject = forecastNode!["TemperatureRanges"]!.AsObject();
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        temperatureRangesObject.WriteTo(writer);
        writer.Flush();
        TemperatureRanges? temperatureRanges = 
            JsonSerializer.Deserialize<TemperatureRanges>(stream.ToArray());
        Console.WriteLine($"Cold.Low={temperatureRanges!["Cold"].Low}, Hot.High={temperatureRanges["Hot"].High}");
        // output:
        //Cold.Low=-10, Hot.High=60

        // Get a subsection and deserialize it into an array.
        JsonArray datesAvailable = forecastNode!["DatesAvailable"]!.AsArray()!;
        Console.WriteLine($"DatesAvailable[0]={datesAvailable[0]}");
        // output:
        //DatesAvailable[0]=8/1/2019 12:00:00 AM
    }
}

Ejemplo de nota media de JsonNode

En el ejemplo siguiente se selecciona una matriz JSON que tiene valores enteros y se calcula un valor medio:

using System.Text.Json.Nodes;

namespace JsonNodeAverageGradeExample;

public class Program
{
    public static void Main()
    {
        string jsonString =
@"{
  ""Class Name"": ""Science"",
  ""Teacher\u0027s Name"": ""Jane"",
  ""Semester"": ""2019-01-01"",
  ""Students"": [
    {
      ""Name"": ""John"",
      ""Grade"": 94.3
    },
    {
      ""Name"": ""James"",
      ""Grade"": 81.0
    },
    {
      ""Name"": ""Julia"",
      ""Grade"": 91.9
    },
    {
      ""Name"": ""Jessica"",
      ""Grade"": 72.4
    },
    {
      ""Name"": ""Johnathan""
    }
  ],
  ""Final"": true
}
";
        double sum = 0;
        int count = 0;

        JsonNode document = JsonNode.Parse(jsonString)!;

        JsonNode root = document.Root;
        JsonArray studentsArray = root["Students"]!.AsArray();

        count = studentsArray.Count;

        foreach (JsonNode? student in studentsArray)
        {
            if (student?["Grade"] is JsonNode gradeNode)
            {
                sum += (double)gradeNode;
            }
            else
            {
                sum += 70;
            }
        }

        double average = sum / count;
        Console.WriteLine($"Average grade : {average}");
    }
}
// output:
//Average grade : 81.92

El código anterior:

  • Calcula la calificación media de los objetos de una matriz de Students que tienen una propiedad Grade.
  • Asigna una calificación predeterminada de 70 a los alumnos que no tienen ninguna calificación.
  • Obtiene el número de alumnos de la propiedad Count de JsonArray.

JsonNode con JsonSerializerOptions

Puede usar JsonSerializer para serializar y deserializar una instancia de JsonNode. Pero si usa una sobrecarga que toma JsonSerializerOptions, la instancia de opciones solo se usa para obtener convertidores personalizados. No se usan otras características de la instancia de opciones. Por ejemplo, si establece JsonSerializerOptions.DefaultIgnoreCondition en WhenWritingNull y llama a JsonSerializer con una sobrecarga que toma JsonSerializerOptions, no se omiten las propiedades null.

La misma limitación se aplica a los métodos JsonNode que toman un parámetro JsonSerializerOptions: WriteTo(Utf8JsonWriter, JsonSerializerOptions) y ToJsonString(JsonSerializerOptions). Estas API solo usan JsonSerializerOptions para obtener convertidores personalizados.

En el ejemplo siguiente se muestra el resultado del uso de métodos que toman un parámetro JsonSerializerOptions y serializan una instancia JsonNode:

using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace JsonNodeWithJsonSerializerOptions;

public class Program
{
    public static void Main()
    {
        Person person = new Person { Name = "Nancy" };

        // Default serialization - Address property included with null token.
        // Output: {"Name":"Nancy","Address":null}
        string personJsonWithNull = JsonSerializer.Serialize(person);
        Console.WriteLine(personJsonWithNull);

        // Serialize and ignore null properties - null Address property is omitted
        // Output: {"Name":"Nancy"}
        JsonSerializerOptions options = new()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
        Console.WriteLine(personJsonWithoutNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonSerializer.
        // Output: {"Name":"Nancy","Address":null}
        var personJsonNode = JsonSerializer.Deserialize<JsonNode>(personJsonWithNull);
        personJsonWithNull = JsonSerializer.Serialize(personJsonNode, options);
        Console.WriteLine(personJsonWithNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonNode.ToJsonString method.
        // Output: {"Name":"Nancy","Address":null}
        personJsonWithNull = personJsonNode!.ToJsonString(options);
        Console.WriteLine(personJsonWithNull);

        // Ignore null properties doesn't work when serializing JsonNode instance
        // by using JsonNode.WriteTo method.
        // Output: {"Name":"Nancy","Address":null}
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        personJsonNode!.WriteTo(writer, options);
        writer.Flush();
        personJsonWithNull = Encoding.UTF8.GetString(stream.ToArray());
        Console.WriteLine(personJsonWithNull);
    }
}
public class Person
{
    public string? Name { get; set; }
    public string? Address { get; set; }
}

Si necesita características de JsonSerializerOptions distintas a los convertidores personalizados, use JsonSerializer con destinos fuertemente tipados (como la clase Person de este ejemplo) en lugar de JsonNode.

Use JsonDocument

En el ejemplo siguiente se muestra cómo usar la clase JsonDocument para el acceso aleatorio a los datos de una cadena JSON:

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");
    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
        count++;
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")
    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
        count += 1
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

El código anterior:

  • Supone que el JSON que se va a analizar está en una cadena denominada jsonString.
  • Calcula la calificación media de los objetos de una matriz de Students que tienen una propiedad Grade.
  • Asigna una calificación predeterminada de 70 a los alumnos que no tienen ninguna calificación.
  • Crea la instancia JsonDocument en using (Instrucción) porque JsonDocument implementa IDisposable. Después de eliminar una instancia JsonDocument, también se pierde el acceso a todas sus instancias JsonElement. Para conservar el acceso a una instancia JsonElement, realice una copia de ella antes de eliminar la instancia primaria JsonDocument. Para realizar una copia, llame a JsonElement.Clone. Para obtener más información, vea JsonDocument es IDisposable.

El código de ejemplo anterior cuenta a los alumnos por medio del incremento de una variable count con cada iteración. Una alternativa es llamar a GetArrayLength, tal y como se muestra en el ejemplo siguiente:

double sum = 0;
int count = 0;

using (JsonDocument document = JsonDocument.Parse(jsonString))
{
    JsonElement root = document.RootElement;
    JsonElement studentsElement = root.GetProperty("Students");

    count = studentsElement.GetArrayLength();

    foreach (JsonElement student in studentsElement.EnumerateArray())
    {
        if (student.TryGetProperty("Grade", out JsonElement gradeElement))
        {
            sum += gradeElement.GetDouble();
        }
        else
        {
            sum += 70;
        }
    }
}

double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
    Dim root As JsonElement = document.RootElement
    Dim studentsElement As JsonElement = root.GetProperty("Students")

    count = studentsElement.GetArrayLength()

    For Each student As JsonElement In studentsElement.EnumerateArray()
        Dim gradeElement As JsonElement = Nothing
        If student.TryGetProperty("Grade", gradeElement) Then
            sum += gradeElement.GetDouble()
        Else
            sum += 70
        End If
    Next
End Using

Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")

Aquí se muestra un ejemplo del código JSON que este código procesa:

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

Para obtener un ejemplo similar que usa JsonNode en lugar de JsonDocument, vea Ejemplo de nota media de JsonNode.

Cómo buscar subelementos en JsonDocument y JsonElement

Las búsquedas en JsonElement requieren una búsqueda secuencial de las propiedades y, por lo tanto, son relativamente lentas (por ejemplo, al usar TryGetProperty). System.Text.Json está diseñado para minimizar el tiempo de análisis inicial en lugar del tiempo de búsqueda. Por lo tanto, use los enfoques siguientes para optimizar el rendimiento al buscar en un objeto JsonDocument:

  • Use los enumeradores integrados (EnumerateArray y EnumerateObject) en lugar de crear sus propios bucles o índices.
  • No realice una búsqueda secuencial en todo el JsonDocument a través de todas las propiedades mediante RootElement. En su lugar, busque objetos JSON anidados en función de la estructura conocida de los datos JSON. Por ejemplo, en los ejemplos de código anteriores se busca una propiedad Grade en objetos Student al recorrer en bucle los objetos Student y obtener el valor de Grade de cada uno, en lugar de buscar en todos los objetos JsonElement las propiedades Grade. Si se hace esto último, se pasaría innecesariamente por los mismos datos.

Uso de JsonDocument para escribir JSON

En el siguiente ejemplo se muestra cómo escribir JSON desde una JsonDocument:

string jsonString = File.ReadAllText(inputFileName);

var writerOptions = new JsonWriterOptions
{
    Indented = true
};

var documentOptions = new JsonDocumentOptions
{
    CommentHandling = JsonCommentHandling.Skip
};

using FileStream fs = File.Create(outputFileName);
using var writer = new Utf8JsonWriter(fs, options: writerOptions);
using JsonDocument document = JsonDocument.Parse(jsonString, documentOptions);

JsonElement root = document.RootElement;

if (root.ValueKind == JsonValueKind.Object)
{
    writer.WriteStartObject();
}
else
{
    return;
}

foreach (JsonProperty property in root.EnumerateObject())
{
    property.WriteTo(writer);
}

writer.WriteEndObject();

writer.Flush();
Dim jsonString As String = File.ReadAllText(inputFileName)

Dim writerOptions As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim documentOptions As JsonDocumentOptions = New JsonDocumentOptions With {
    .CommentHandling = JsonCommentHandling.Skip
}

Dim fs As FileStream = File.Create(outputFileName)
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(fs, options:=writerOptions)
Dim document As JsonDocument = JsonDocument.Parse(jsonString, documentOptions)

Dim root As JsonElement = document.RootElement

If root.ValueKind = JsonValueKind.[Object] Then
    writer.WriteStartObject()
Else
    Return
End If

For Each [property] As JsonProperty In root.EnumerateObject()
    [property].WriteTo(writer)
Next

writer.WriteEndObject()

writer.Flush()

El código anterior:

  • Lee un archivo JSON, carga los datos en un JsonDocument y escribe JSON con formato (impreso correctamente) en un archivo.
  • Utiliza JsonDocumentOptions para especificar que se permiten los comentarios en el JSON de entrada, pero se omiten.
  • Cuando termina, llama a Flush en el escritor. Una alternativa consiste en permitir el vaciado automático del escritor cuando se elimina.

Aquí se muestra un ejemplo de entrada JSON que el código de ejemplo va a procesar:

{"Class Name": "Science","Teacher's Name": "Jane","Semester": "2019-01-01","Students": [{"Name": "John","Grade": 94.3},{"Name": "James","Grade": 81.0},{"Name": "Julia","Grade": 91.9},{"Name": "Jessica","Grade": 72.4},{"Name": "Johnathan"}],"Final": true}

El resultado es la siguiente salida JSON impresa correctamente:

{
  "Class Name": "Science",
  "Teacher\u0027s Name": "Jane",
  "Semester": "2019-01-01",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ],
  "Final": true
}

JsonDocument es IDisposable

JsonDocument compila una vista en memoria de los datos en un búfer agrupado. Por lo tanto, el tipo JsonDocument implementa IDisposable y debe usarse dentro de un bloque using.

Devuelva un JsonDocument desde la API solo si quiere transferir la propiedad de la duración y derivar la responsabilidad al autor de la llamada. En la mayoría de los escenarios, eso no es necesario. Si el autor de la llamada necesita trabajar con todo el documento JSON, devuelva el Clone del RootElement, que es un JsonElement. Si el autor de la llamada necesita trabajar con un elemento determinado dentro del documento JSON, devuelva el Clone de dicho JsonElement. Si devuelve el RootElement o un subelemento directamente sin realizar un Clone, el autor de la llamada no podrá acceder al JsonElement devuelto después de que se elimine el JsonDocument que lo posee.

Este es un ejemplo en el que se le requiere que realice un Clone:

public JsonElement LookAndLoad(JsonElement source)
{
    string json = File.ReadAllText(source.GetProperty("fileName").GetString());

    using (JsonDocument doc = JsonDocument.Parse(json))
    {
        return doc.RootElement.Clone();
    }
}

El código anterior espera un JsonElement que contiene una propiedad fileName. Abre el archivo JSON y crea un JsonDocument. El método supone que el autor de la llamada quiere trabajar con todo el documento, por lo que devuelve el Clone del RootElement.

Si recibe un JsonElement y está devolviendo un subelemento, no es necesario devolver un Clone del subelemento. El autor de la llamada es responsable de mantener activo el JsonDocument al que pertenece el JsonElement pasado. Por ejemplo:

public JsonElement ReturnFileName(JsonElement source)
{
   return source.GetProperty("fileName");
}

JsonDocument con JsonSerializerOptions

Puede usar JsonSerializer para serializar y deserializar una instancia de JsonDocument. Pero la implementación para leer y escribir instancias JsonDocument mediante JsonSerializer es un contenedor en torno a JsonDocument.ParseValue(Utf8JsonReader) y JsonDocument.WriteTo(Utf8JsonWriter). Este contenedor no reenvía JsonSerializerOptions (características de serializador) a Utf8JsonReader o Utf8JsonWriter. Por ejemplo, si establece JsonSerializerOptions.DefaultIgnoreCondition en WhenWritingNull y llama a JsonSerializer con una sobrecarga que toma JsonSerializerOptions, no se omiten las propiedades null.

En el ejemplo siguiente se muestra el resultado del uso de métodos que toman un parámetro JsonSerializerOptions y serializan una instancia JsonDocument:

using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace JsonDocumentWithJsonSerializerOptions;

public class Program
{
    public static void Main()
    {
        Person person = new Person { Name = "Nancy" };

        // Default serialization - Address property included with null token.
        // Output: {"Name":"Nancy","Address":null}
        string personJsonWithNull = JsonSerializer.Serialize(person);
        Console.WriteLine(personJsonWithNull);

        // Serialize and ignore null properties - null Address property is omitted
        // Output: {"Name":"Nancy"}
        JsonSerializerOptions options = new()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
        string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
        Console.WriteLine(personJsonWithoutNull);

        // Ignore null properties doesn't work when serializing JsonDocument instance
        // by using JsonSerializer.
        // Output: {"Name":"Nancy","Address":null}
        var personJsonDocument = JsonSerializer.Deserialize<JsonDocument>(personJsonWithNull);
        personJsonWithNull = JsonSerializer.Serialize(personJsonDocument, options);
        Console.WriteLine(personJsonWithNull);
    }
}
public class Person
{
    public string? Name { get; set; }

    public string? Address { get; set; }
}

Si necesita características de JsonSerializerOptions, use JsonSerializer con destinos fuertemente tipados (como la clase Person de este ejemplo) en lugar de JsonDocument.

Use Utf8JsonWriter

Utf8JsonWriter ofrece una forma de escribir texto JSON con codificación UTF-8 de alto rendimiento a partir de tipos de .NET comunes como String, Int32 y DateTime. El escritor es un tipo de bajo nivel que se puede usar para compilar serializadores personalizados. El método JsonSerializer.Serialize usa Utf8JsonWriter en segundo plano.

En el siguiente ejemplo, se muestra cómo utilizar la clase Utf8JsonWriter:

var options = new JsonWriterOptions
{
    Indented = true
};

using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);

writer.WriteStartObject();
writer.WriteString("date", DateTimeOffset.UtcNow);
writer.WriteNumber("temp", 42);
writer.WriteEndObject();
writer.Flush();

string json = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
Dim options As JsonWriterOptions = New JsonWriterOptions With {
    .Indented = True
}

Dim stream As MemoryStream = New MemoryStream
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(stream, options)

writer.WriteStartObject()
writer.WriteString("date", DateTimeOffset.UtcNow)
writer.WriteNumber("temp", 42)
writer.WriteEndObject()
writer.Flush()

Dim json As String = Encoding.UTF8.GetString(stream.ToArray())
Console.WriteLine(json)

Escritura con texto UTF-8

Para lograr el mejor rendimiento posible mientras usa Utf8JsonWriter, escriba cargas de JSON ya codificadas como texto UTF-8 en lugar de como cadenas UTF-16. Utilice JsonEncodedText para almacenar en caché y codificar previamente los nombres y valores de las propiedades de cadena conocidas como estáticos y pasarlos al escritor, en lugar de usar literales de cadena UTF-16. Esto es más rápido que el almacenamiento en caché y el uso de matrices de bytes UTF-8.

Este enfoque también funciona si necesita realizar un escape personalizado. System.Text.Json no permite deshabilitar el escape mientras se escribe una cadena, pero podría pasar su propio JavaScriptEncoder personalizado como una opción al escritor, o crear su propio JsonEncodedText que use su JavascriptEncoder para realizar el escape y, después, escribir el JsonEncodedText en lugar de la cadena. Para más información, vea Personalización de la codificación de caracteres.

Escritura de JSON sin formato

En algunos escenarios, es posible que quiera escribir JSON "sin formato" en una carga JSON que esté creando con Utf8JsonWriter. Puede usar Utf8JsonWriter.WriteRawValue para ello. Estos son escenarios típicos:

  • Tiene una carga JSON existente que quiere incluir en nuevo JSON.

  • Quiere aplicar un formato diferente al predeterminado Utf8JsonWriter a los valores.

    Por ejemplo, puede que quiera personalizar el formato de número. De manera predeterminada, System.Text.Json omite el separador decimal de los números enteros, escribiendo 1 en lugar de 1.0, por ejemplo. La lógica es que escribir menos bytes es bueno para el rendimiento. Pero imagine que el consumidor de JSON trata los números con decimales como valores double y los números sin decimales como enteros. Es posible que quiera asegurarse de que todos los números de una matriz se reconozcan como valores double escribiendo un separador decimal y cero para los números enteros. En el siguiente ejemplo se muestra cómo hacerlo:

    using System.Text;
    using System.Text.Json;
    
    namespace WriteRawJson;
    
    public class Program
    {
        public static void Main()
        {
            JsonWriterOptions writerOptions = new() { Indented = true, };
    
            using MemoryStream stream = new();
            using Utf8JsonWriter writer = new(stream, writerOptions);
    
            writer.WriteStartObject();
    
            writer.WriteStartArray("defaultJsonFormatting");
            foreach (double number in new double[] { 50.4, 51 })
            {
                writer.WriteStartObject();
                writer.WritePropertyName("value");
                writer.WriteNumberValue(number);
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
    
            writer.WriteStartArray("customJsonFormatting");
            foreach (double result in new double[] { 50.4, 51 })
            {
                writer.WriteStartObject();
                writer.WritePropertyName("value");
                writer.WriteRawValue(
                    FormatNumberValue(result), skipInputValidation: true);
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
    
            writer.WriteEndObject();
            writer.Flush();
    
            string json = Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(json);
        }
        static string FormatNumberValue(double numberValue)
        {
            return numberValue == Convert.ToInt32(numberValue) ? 
                numberValue.ToString() + ".0" : numberValue.ToString();
        }
    }
    // output:
    //{
    //  "defaultJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51
    //    }
    //  ],
    //  "customJsonFormatting": [
    //    {
    //      "value": 50.4
    //    },
    //    {
    //      "value": 51.0
    //    }
    //  ]
    //}
    

Personalización del escape de caracteres

El valor StringEscapeHandling de JsonTextWriter ofrece opciones para escapar todos los caracteres que no sean ASCII o caracteres HTML. De forma predeterminada, Utf8JsonWriter convierte todos los caracteres que no son ASCII y HTML. Este escape se hace por motivos de seguridad de defensa en profundidad. Para especificar una directiva de escape diferente, cree un JavaScriptEncoder y configure JsonWriterOptions.Encoder. Para más información, vea Personalización de la codificación de caracteres.

Escritura de valores NULL

Para escribir valores NULL mediante Utf8JsonWriter, llame a:

  • WriteNull para escribir un par clave-valor con NULL como valor.
  • WriteNullValue para escribir NULL como un elemento de una matriz JSON.

En el caso de una propiedad de cadena, si la cadena es NULL, WriteString y WriteStringValue son equivalentes a WriteNull y WriteNullValue.

Escritura de valores TimeSpan, URI o char

Para escribir valores Timespan, Uri o char, dé formato a estos valores como cadenas (por ejemplo, llamando a ToString()) y llame a WriteStringValue.

Use Utf8JsonReader

Utf8JsonReader es un lector de solo avance, de baja asignación y de alto rendimiento para texto JSON con codificación UTF-8 que se lee desde ReadOnlySpan<byte> o ReadOnlySequence<byte>. Utf8JsonReader es un tipo de bajo nivel que se puede usar para compilar analizadores y deserializadores personalizados. El método JsonSerializer.Deserialize usa Utf8JsonReader en segundo plano.

Utf8JsonReader no se puede usar directamente desde el código de Visual Basic. Para obtener más información, vea Compatibilidad con Visual Basic.

En el siguiente ejemplo, se muestra cómo utilizar la clase Utf8JsonReader:

var options = new JsonReaderOptions
{
    AllowTrailingCommas = true,
    CommentHandling = JsonCommentHandling.Skip
};
var reader = new Utf8JsonReader(jsonUtf8Bytes, options);

while (reader.Read())
{
    Console.Write(reader.TokenType);

    switch (reader.TokenType)
    {
        case JsonTokenType.PropertyName:
        case JsonTokenType.String:
            {
                string? text = reader.GetString();
                Console.Write(" ");
                Console.Write(text);
                break;
            }

        case JsonTokenType.Number:
            {
                int intValue = reader.GetInt32();
                Console.Write(" ");
                Console.Write(intValue);
                break;
            }

            // Other token types elided for brevity
    }
    Console.WriteLine();
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

En el código anterior se supone que la variable jsonUtf8 es una matriz de bytes que contiene JSON válido, con codificación UTF-8.

Filtrado de datos mediante Utf8JsonReader

En el ejemplo siguiente se muestra cómo leer un archivo de forma sincrónica y buscar un valor.

using System.Text;
using System.Text.Json;

namespace SystemTextJsonSamples
{
    public class Utf8ReaderFromFile
    {
        private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
        private static ReadOnlySpan<byte> Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF };

        public static void Run()
        {
            // ReadAllBytes if the file encoding is UTF-8:
            string fileName = "UniversitiesUtf8.json";
            ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);

            // Read past the UTF-8 BOM bytes if a BOM exists.
            if (jsonReadOnlySpan.StartsWith(Utf8Bom))
            {
                jsonReadOnlySpan = jsonReadOnlySpan.Slice(Utf8Bom.Length);
            }

            // Or read as UTF-16 and transcode to UTF-8 to convert to a ReadOnlySpan<byte>
            //string fileName = "Universities.json";
            //string jsonString = File.ReadAllText(fileName);
            //ReadOnlySpan<byte> jsonReadOnlySpan = Encoding.UTF8.GetBytes(jsonString);

            int count = 0;
            int total = 0;

            var reader = new Utf8JsonReader(jsonReadOnlySpan);

            while (reader.Read())
            {
                JsonTokenType tokenType = reader.TokenType;

                switch (tokenType)
                {
                    case JsonTokenType.StartObject:
                        total++;
                        break;
                    case JsonTokenType.PropertyName:
                        if (reader.ValueTextEquals(s_nameUtf8))
                        {
                            // Assume valid JSON, known schema
                            reader.Read();
                            if (reader.GetString()!.EndsWith("University"))
                            {
                                count++;
                            }
                        }
                        break;
                }
            }
            Console.WriteLine($"{count} out of {total} have names that end with 'University'");
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

Puede encontrar una versión asincrónica de este ejemplo en Proyecto JSON de ejemplos de .NET.

El código anterior:

  • Supone que el JSON contiene una matriz de objetos y cada objeto puede contener una propiedad "name" de tipo cadena.

  • Cuenta los objetos y los valores de propiedad "name" que terminan en "University".

  • Supone que el archivo tiene codificación UTF-16 y lo transcodifica a UTF-8. Un archivo con codificación UTF-8 puede leerse directamente en ReadOnlySpan<byte> mediante el código siguiente:

    ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
    

    Si el archivo contiene una marca BOM UTF-8, quítela antes de pasar los bytes a Utf8JsonReader, ya que el lector espera texto. De lo contrario, la marca BOM se considera JSON no válido y el lector inicia una excepción.

Aquí se muestra un ejemplo de JSON que el código anterior puede leer. El mensaje de resumen resultante es "2 de 4 tienen nombres que terminan en 'University'":

[
  {
    "web_pages": [ "https://contoso.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contoso.edu" ],
    "name": "Contoso Community College"
  },
  {
    "web_pages": [ "http://fabrikam.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikam.edu" ],
    "name": "Fabrikam Community College"
  },
  {
    "web_pages": [ "http://www.contosouniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contosouniversity.edu" ],
    "name": "Contoso University"
  },
  {
    "web_pages": [ "http://www.fabrikamuniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikamuniversity.edu" ],
    "name": "Fabrikam University"
  }
]

Consumo de cadenas JSON descodificadas

A partir de .NET 7, puede usar el método Utf8JsonReader.CopyString en lugar de Utf8JsonReader.GetString() para consumir una cadena JSON descodificada. A diferencia de GetString(), que siempre asigna una nueva cadena, CopyString le permite copiar la cadena sin escape en un búfer de su propiedad. En el fragmento de código siguiente se muestra un ejemplo de consumo de una cadena UTF-16 mediante CopyString.

int valueLength = reader.HasReadOnlySequence 
    ? checked((int)ValueSequence.Length) 
    : ValueSpan.Length;

char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);

// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);

Lectura de una secuencia mediante Utf8JsonReader

Al leer un archivo grande (un gigabyte o más de tamaño, por ejemplo), puede que desee evitar tener que cargar todo el archivo en la memoria de una vez. En este escenario, puede usar FileStream.

Al usar Utf8JsonReader para leer de una secuencia, se aplican las siguientes reglas:

  • El búfer que contiene la carga parcial JSON debe ser al menos tan grande como el token JSON más grande que contiene para que el lector pueda avanzar.
  • El búfer debe ser al menos tan grande como la secuencia más grande de espacio en blanco dentro del JSON.
  • El lector no realiza un seguimiento de los datos que ha leído hasta que lea completamente el TokenType siguiente en la carga JSON. Por tanto, cuando haya bytes restantes en el búfer, tendrá que volver a pasarlos al lector. Puede usar BytesConsumed para determinar el número de bytes que quedan.

El código siguiente muestra cómo leer desde una secuencia. Este ejemplo se muestra MemoryStream. Un código similar funcionará con FileStream, excepto cuando FileStream contenga una marca BOM UTF-8 al principio. En ese caso, debe quitar esos tres bytes del búfer antes de pasar los bytes restantes a Utf8JsonReader. En caso contrario, el lector produciría una excepción, ya que la marca BOM no se considera una parte válida del JSON.

El código de ejemplo comienza con un búfer de 4 KB y duplica el tamaño del búfer cada vez que encuentra que el tamaño no es lo suficientemente grande como para ajustarse a un token JSON completo, lo que es necesario para que el lector realice el progreso de la carga de JSON. El ejemplo de JSON proporcionado en el fragmento de código desencadena un aumento del tamaño del búfer solo si se establece un tamaño de búfer inicial muy pequeño, por ejemplo, 10 bytes. Si establece el tamaño de búfer inicial en 10, las instrucciones Console.WriteLine muestran la causa y el efecto de los aumentos del tamaño del búfer. En el tamaño de búfer inicial de 4 KB, se muestra todo el JSON de ejemplo en cada Console.WriteLine y el tamaño del búfer nunca se debe aumentar.

using System.Text;
using System.Text.Json;

namespace SystemTextJsonSamples
{
    public class Utf8ReaderPartialRead
    {
        public static void Run()
        {
            var jsonString = @"{
                ""Date"": ""2019-08-01T00:00:00-07:00"",
                ""Temperature"": 25,
                ""TemperatureRanges"": {
                    ""Cold"": { ""High"": 20, ""Low"": -10 },
                    ""Hot"": { ""High"": 60, ""Low"": 20 }
                },
                ""Summary"": ""Hot"",
            }";

            byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
            var stream = new MemoryStream(bytes);

            var buffer = new byte[4096];

            // Fill the buffer.
            // For this snippet, we're assuming the stream is open and has data.
            // If it might be closed or empty, check if the return value is 0.
            stream.Read(buffer);

            // We set isFinalBlock to false since we expect more data in a subsequent read from the stream.
            var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");

            // Search for "Summary" property name
            while (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals("Summary"))
            {
                if (!reader.Read())
                {
                    // Not enough of the JSON is in the buffer to complete a read.
                    GetMoreBytesFromStream(stream, buffer, ref reader);
                }
            }

            // Found the "Summary" property name.
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            while (!reader.Read())
            {
                // Not enough of the JSON is in the buffer to complete a read.
                GetMoreBytesFromStream(stream, buffer, ref reader);
            }
            // Display value of Summary property, that is, "Hot".
            Console.WriteLine($"Got property value: {reader.GetString()}");
        }

        private static void GetMoreBytesFromStream(
            MemoryStream stream, byte[] buffer, ref Utf8JsonReader reader)
        {
            int bytesRead;
            if (reader.BytesConsumed < buffer.Length)
            {
                ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed);

                if (leftover.Length == buffer.Length)
                {
                    Array.Resize(ref buffer, buffer.Length * 2);
                    Console.WriteLine($"Increased buffer size to {buffer.Length}");
                }

                leftover.CopyTo(buffer);
                bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
            }
            else
            {
                bytesRead = stream.Read(buffer);
            }
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

En el ejemplo anterior no se establece ningún límite para el tamaño del búfer. Si el tamaño del token es demasiado grande, se podría producir un error en el código con una excepción OutOfMemoryException. Esto puede ocurrir si el archivo JSON contiene un token de aproximadamente 1 GB o más de tamaño, ya que la duplicación del tamaño de 1 GB da como resultado un tamaño demasiado grande para caber en un búfer de int32.

Utf8JsonReader es una estructura de referencia

Dado que el tipo Utf8JsonReader es una estructura de referencia, tiene ciertas limitaciones. Por ejemplo, no se puede almacenar como un campo en una clase o estructura que no sea una estructura de referencia. Para lograr un alto rendimiento, este tipo debe ser ref struct, ya que necesita almacenar en caché la entrada ReadOnlySpan<byte>, que en sí misma es una estructura de referencia. Además, este tipo es mutable ya que contiene el estado; Por tanto, páselo por referencia en lugar de por valor. Si se pasa por valor, se producirá una copia de la estructura y los cambios de estado no serán visibles para el autor de la llamada. Para más información sobre el uso de las estructuras de referencia, vea Evitar asignaciones.

Lectura de texto UTF-8

Para lograr el mejor rendimiento posible mientras usa Utf8JsonReader, lea cargas de JSON ya codificadas como texto UTF-8 en lugar de como cadenas UTF-16. Para obtener un ejemplo de código, vea Filtrado de datos mediante Utf8JsonReader.

Lectura con ReadOnlySequence de varios segmentos

Si la entrada JSON es ReadOnlySpan<byte>, se puede acceder a cada elemento JSON desde la propiedad ValueSpan en el lector a medida que se avanza por el bucle de lectura. Pero si la entrada es ReadOnlySequence<byte> (que es el resultado de la lectura de PipeReader), algunos elementos JSON podrían ocupar varios segmentos del objeto ReadOnlySequence<byte>. No se puede acceder a estos elementos desde ValueSpan en un bloque de memoria contiguo. En su lugar, siempre que tenga un ReadOnlySequence<byte> de varios segmentos como entrada, sondee la propiedad HasValueSequence en el lector para averiguar cómo acceder al elemento JSON actual. Este es un patrón recomendado:

while (reader.Read())
{
    switch (reader.TokenType)
    {
        // ...
        ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
            reader.ValueSequence.ToArray() :
            reader.ValueSpan;
        // ...
    }
}

Uso de ValueTextEquals para las búsquedas de nombres de propiedad

No use ValueSpan para realizar comparaciones byte a byte mediante una llamada a SequenceEqual para las búsquedas de nombres de propiedad. En su lugar, llame a ValueTextEquals, ya que ese método anula el escape de caracteres que se van a escapar en JSON. Este es un ejemplo en el que se muestra cómo buscar una propiedad denominada "name":

private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
    switch (reader.TokenType)
    {
        case JsonTokenType.StartObject:
            total++;
            break;
        case JsonTokenType.PropertyName:
            if (reader.ValueTextEquals(s_nameUtf8))
            {
                count++;
            }
            break;
    }
}

Lectura de valores NULL en tipos de valor que aceptan valores NULL

Las API integradas de System.Text.Json solo devuelven tipos de valor que no aceptan valores NULL. Por ejemplo, Utf8JsonReader.GetBoolean devuelve bool. Si encuentra Null en el elemento JSON, inicia una excepción. En los siguientes ejemplos se muestran dos formas de controlar valores NULL: una devolviendo un tipo de valor que acepta valores NULL y otra devolviendo el valor predeterminado:

public bool? ReadAsNullableBoolean()
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return null;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return defaultValue;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}

Vea también