Parsing a JSON File with .Net Core 3.0/System.Text.JSON

Parsing a JSON file with .NET core 3.0/System.text.Json

Update 2019-10-13: Rewritten the Utf8JsonStreamReader to use ReadOnlySequences internally, added wrapper for JsonSerializer.Deserialize method.


I have created a wrapper around Utf8JsonReader for exactly this purpose:

public ref struct Utf8JsonStreamReader
{
private readonly Stream _stream;
private readonly int _bufferSize;

private SequenceSegment? _firstSegment;
private int _firstSegmentStartIndex;
private SequenceSegment? _lastSegment;
private int _lastSegmentEndIndex;

private Utf8JsonReader _jsonReader;
private bool _keepBuffers;
private bool _isFinalBlock;

public Utf8JsonStreamReader(Stream stream, int bufferSize)
{
_stream = stream;
_bufferSize = bufferSize;

_firstSegment = null;
_firstSegmentStartIndex = 0;
_lastSegment = null;
_lastSegmentEndIndex = -1;

_jsonReader = default;
_keepBuffers = false;
_isFinalBlock = false;
}

public bool Read()
{
// read could be unsuccessful due to insufficient bufer size, retrying in loop with additional buffer segments
while (!_jsonReader.Read())
{
if (_isFinalBlock)
return false;

MoveNext();
}

return true;
}

private void MoveNext()
{
var firstSegment = _firstSegment;
_firstSegmentStartIndex += (int)_jsonReader.BytesConsumed;

// release previous segments if possible
if (!_keepBuffers)
{
while (firstSegment?.Memory.Length <= _firstSegmentStartIndex)
{
_firstSegmentStartIndex -= firstSegment.Memory.Length;
firstSegment.Dispose();
firstSegment = (SequenceSegment?)firstSegment.Next;
}
}

// create new segment
var newSegment = new SequenceSegment(_bufferSize, _lastSegment);

if (firstSegment != null)
{
_firstSegment = firstSegment;
newSegment.Previous = _lastSegment;
_lastSegment?.SetNext(newSegment);
_lastSegment = newSegment;
}
else
{
_firstSegment = _lastSegment = newSegment;
_firstSegmentStartIndex = 0;
}

// read data from stream
_lastSegmentEndIndex = _stream.Read(newSegment.Buffer.Memory.Span);
_isFinalBlock = _lastSegmentEndIndex < newSegment.Buffer.Memory.Length;
_jsonReader = new Utf8JsonReader(new ReadOnlySequence<byte>(_firstSegment, _firstSegmentStartIndex, _lastSegment, _lastSegmentEndIndex), _isFinalBlock, _jsonReader.CurrentState);
}

public T Deserialize<T>(JsonSerializerOptions? options = null)
{
// JsonSerializer.Deserialize can read only a single object. We have to extract
// object to be deserialized into separate Utf8JsonReader. This incures one additional
// pass through data (but data is only passed, not parsed).
var tokenStartIndex = _jsonReader.TokenStartIndex;
var firstSegment = _firstSegment;
var firstSegmentStartIndex = _firstSegmentStartIndex;

// loop through data until end of object is found
_keepBuffers = true;
int depth = 0;

if (TokenType == JsonTokenType.StartObject || TokenType == JsonTokenType.StartArray)
depth++;

while (depth > 0 && Read())
{
if (TokenType == JsonTokenType.StartObject || TokenType == JsonTokenType.StartArray)
depth++;
else if (TokenType == JsonTokenType.EndObject || TokenType == JsonTokenType.EndArray)
depth--;
}

_keepBuffers = false;

// end of object found, extract json reader for deserializer
var newJsonReader = new Utf8JsonReader(new ReadOnlySequence<byte>(firstSegment!, firstSegmentStartIndex, _lastSegment!, _lastSegmentEndIndex).Slice(tokenStartIndex, _jsonReader.Position), true, default);

// deserialize value
var result = JsonSerializer.Deserialize<T>(ref newJsonReader, options);

// release memory if possible
firstSegmentStartIndex = _firstSegmentStartIndex + (int)_jsonReader.BytesConsumed;

while (firstSegment?.Memory.Length < firstSegmentStartIndex)
{
firstSegmentStartIndex -= firstSegment.Memory.Length;
firstSegment.Dispose();
firstSegment = (SequenceSegment?)firstSegment.Next;
}

if (firstSegment != _firstSegment)
{
_firstSegment = firstSegment;
_firstSegmentStartIndex = firstSegmentStartIndex;
_jsonReader = new Utf8JsonReader(new ReadOnlySequence<byte>(_firstSegment!, _firstSegmentStartIndex, _lastSegment!, _lastSegmentEndIndex), _isFinalBlock, _jsonReader.CurrentState);
}

return result;
}

public void Dispose() =>_lastSegment?.Dispose();

public int CurrentDepth => _jsonReader.CurrentDepth;
public bool HasValueSequence => _jsonReader.HasValueSequence;
public long TokenStartIndex => _jsonReader.TokenStartIndex;
public JsonTokenType TokenType => _jsonReader.TokenType;
public ReadOnlySequence<byte> ValueSequence => _jsonReader.ValueSequence;
public ReadOnlySpan<byte> ValueSpan => _jsonReader.ValueSpan;

public bool GetBoolean() => _jsonReader.GetBoolean();
public byte GetByte() => _jsonReader.GetByte();
public byte[] GetBytesFromBase64() => _jsonReader.GetBytesFromBase64();
public string GetComment() => _jsonReader.GetComment();
public DateTime GetDateTime() => _jsonReader.GetDateTime();
public DateTimeOffset GetDateTimeOffset() => _jsonReader.GetDateTimeOffset();
public decimal GetDecimal() => _jsonReader.GetDecimal();
public double GetDouble() => _jsonReader.GetDouble();
public Guid GetGuid() => _jsonReader.GetGuid();
public short GetInt16() => _jsonReader.GetInt16();
public int GetInt32() => _jsonReader.GetInt32();
public long GetInt64() => _jsonReader.GetInt64();
public sbyte GetSByte() => _jsonReader.GetSByte();
public float GetSingle() => _jsonReader.GetSingle();
public string GetString() => _jsonReader.GetString();
public uint GetUInt32() => _jsonReader.GetUInt32();
public ulong GetUInt64() => _jsonReader.GetUInt64();
public bool TryGetDecimal(out byte value) => _jsonReader.TryGetByte(out value);
public bool TryGetBytesFromBase64(out byte[] value) => _jsonReader.TryGetBytesFromBase64(out value);
public bool TryGetDateTime(out DateTime value) => _jsonReader.TryGetDateTime(out value);
public bool TryGetDateTimeOffset(out DateTimeOffset value) => _jsonReader.TryGetDateTimeOffset(out value);
public bool TryGetDecimal(out decimal value) => _jsonReader.TryGetDecimal(out value);
public bool TryGetDouble(out double value) => _jsonReader.TryGetDouble(out value);
public bool TryGetGuid(out Guid value) => _jsonReader.TryGetGuid(out value);
public bool TryGetInt16(out short value) => _jsonReader.TryGetInt16(out value);
public bool TryGetInt32(out int value) => _jsonReader.TryGetInt32(out value);
public bool TryGetInt64(out long value) => _jsonReader.TryGetInt64(out value);
public bool TryGetSByte(out sbyte value) => _jsonReader.TryGetSByte(out value);
public bool TryGetSingle(out float value) => _jsonReader.TryGetSingle(out value);
public bool TryGetUInt16(out ushort value) => _jsonReader.TryGetUInt16(out value);
public bool TryGetUInt32(out uint value) => _jsonReader.TryGetUInt32(out value);
public bool TryGetUInt64(out ulong value) => _jsonReader.TryGetUInt64(out value);

private sealed class SequenceSegment : ReadOnlySequenceSegment<byte>, IDisposable
{
internal IMemoryOwner<byte> Buffer { get; }
internal SequenceSegment? Previous { get; set; }
private bool _disposed;

public SequenceSegment(int size, SequenceSegment? previous)
{
Buffer = MemoryPool<byte>.Shared.Rent(size);
Previous = previous;

Memory = Buffer.Memory;
RunningIndex = previous?.RunningIndex + previous?.Memory.Length ?? 0;
}

public void SetNext(SequenceSegment next) => Next = next;

public void Dispose()
{
if (!_disposed)
{
_disposed = true;
Buffer.Dispose();
Previous?.Dispose();
}
}
}
}

You can use it as replacement for Utf8JsonReader, or for deserializing json into typed objects (as wrapper around System.Text.Json.JsonSerializer.Deserialize).

Example of usage for deserializing objects from huge JSON array:

using var stream = new FileStream("LargeData.json", FileMode.Open, FileAccess.Read);
using var jsonStreamReader = new Utf8JsonStreamReader(stream, 32 * 1024);

jsonStreamReader.Read(); // move to array start
jsonStreamReader.Read(); // move to start of the object

while (jsonStreamReader.TokenType != JsonTokenType.EndArray)
{
// deserialize object
var obj = jsonStreamReader.Deserialize<TestData>();

// JsonSerializer.Deserialize ends on last token of the object parsed,
// move to the first token of next object
jsonStreamReader.Read();
}

Deserialize method reads data from stream until it finds end of the current object. Then it constructs a new Utf8JsonReader with data read and calls JsonSerializer.Deserialize.

Other methods are passed through to Utf8JsonReader.

And, as always, don't forget to dispose your objects at the end.

How to deserialize part of json using System.Text.Json in .net core 3.0?

Your JSON root object consists of certain fixed keys ("sol_keys" and "validity_checks") whose values each have some fixed schema, and any number of variable keys (the "782" numeric keys) whose values all share a common schema that differs from the schemas of the fixed key values:

{
"782": {
// Properties corresponding to your MarsWheather object
},
"783": {
// Properties corresponding to your MarsWheather object
},
// Other variable numeric key/value pairs corresponding to KeyValuePair<string, MarsWheather>
"sol_keys": [
// Some array values you don't care about
],
"validity_checks": {
// Some object you don't care about
}
}

You would like to deserialize just the variable keys, but when you try to deserialize to a Dictionary<string, MarsWheather> you get an exception because the serializer tries to deserialize a fixed key value as if it were variable key value -- but since the fixed key has an array value while the variable keys have object values, an exception gets thrown. How can System.Text.Json be told to skip the known, fixed keys rather than trying to deserialize them?

If you want to deserialize just the variable keys and skip the fixed, known keys, you will need to create a custom JsonConverter. The easiest way to do that would be to first create some root object for your dictionary:

[JsonConverter(typeof(MarsWheatherRootObjectConverter))]
public class MarsWheatherRootObject
{
public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
}

And then define the following converter for it as follows:

public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
{
static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new Dictionary<string, ReadFixedKeyMethod>(StringComparer.OrdinalIgnoreCase)
{
{ "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
{ "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
};

protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => Enumerable.Empty<KeyValuePair<string, WriteFixedKeyMethod>>();
}

public abstract class FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> : JsonConverter<TObject>
where TDictionary : class, IDictionary<string, TValue>, new()
where TObject : new()
{
protected delegate void ReadFixedKeyMethod(ref Utf8JsonReader reader, TObject obj, string name, JsonSerializerOptions options);
protected delegate void WriteFixedKeyMethod(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options);

protected abstract TDictionary GetDictionary(TObject obj);
protected abstract void SetDictionary(TObject obj, TDictionary dictionary);
protected abstract bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method);
protected abstract IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options);

public override TObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return (typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) == null)
? throw new JsonException(string.Format("Unepected token {0}", reader.TokenType))
: default(TObject);
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
var obj = new TObject();
var dictionary = GetDictionary(obj);
var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
var name = reader.GetString();
reader.ReadAndAssert();
if (TryGetFixedKeyReadMethod(name, options, out var method))
{
method(ref reader, obj, name, options);
}
else
{
if (dictionary == null)
SetDictionary(obj, dictionary = new TDictionary());
dictionary.Add(name, valueConverter.ReadOrDeserialize(ref reader, typeof(TValue), options));
}
}
else if (reader.TokenType == JsonTokenType.EndObject)
{
return obj;
}
else
{
throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
}
}
throw new JsonException(); // Truncated file
}

public override void Write(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options)
{
writer.WriteStartObject();
var dictionary = GetDictionary(value);
if (dictionary != null)
{
var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
foreach (var pair in dictionary)
{
// TODO: handle DictionaryKeyPolicy
writer.WritePropertyName(pair.Key);
valueConverter.WriteOrSerialize(writer, pair.Value, typeof(TValue), options);
}
}
foreach (var pair in GetFixedKeyWriteMethods(options))
{
writer.WritePropertyName(pair.Key);
pair.Value(writer, value, options);
}
writer.WriteEndObject();
}
}

public static partial class JsonExtensions
{
public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, Type type, JsonSerializerOptions options)
{
if (converter != null)
converter.Write(writer, value, options);
else
JsonSerializer.Serialize(writer, value, type, options);
}

public static T ReadOrDeserialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> converter != null ? converter.Read(ref reader, typeToConvert, options) : (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);

public static void ReadAndAssert(this ref Utf8JsonReader reader)
{
if (!reader.Read())
throw new JsonException();
}
}

And now you will be able to deserialize to MarsWheatherRootObject as follows:

var root = await System.Text.Json.JsonSerializer.DeserializeAsync<MarsWheatherRootObject>(
stream,
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});

Demo fiddle #1 here.

Notes:

  • FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> provides a general framework for serializing and deserializing objects with fixed and variable properties. If later you decide to deserialize e.g. "sol_keys", you could modify MarsWheatherRootObject as follows:

    [JsonConverter(typeof(MarsWheatherRootObjectConverter))]
    public class MarsWheatherRootObject
    {
    public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
    public List<string> SolKeys { get; set; } = new List<string>();
    }

    And the converter as follows:

    public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
    {
    static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new(StringComparer.OrdinalIgnoreCase)
    {
    { "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) =>
    {
    obj.SolKeys = JsonSerializer.Deserialize<List<string>>(ref reader, options);
    }
    },
    { "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
    };
    static readonly Dictionary<string, WriteFixedKeyMethod> FixedKeyWriteMethods = new Dictionary<string, WriteFixedKeyMethod>()
    {
    { "sol_keys", (w, v, o) =>
    {
    JsonSerializer.Serialize(w, v.SolKeys, o);
    }
    },
    };

    protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
    protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
    protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
    protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => FixedKeyWriteMethods;
    }

    Demo fiddle #2 here.

How to read JSON file into string specifying values and using System.Text.Json?

JsonSerializer.Deserialize expects a JSON string, not the name of a file.

You need to load the file into a string and then pass that to the Deserialize method:

var json = File.ReadAllText("appsettings.json");
var dict = JsonSerializer.Deserialize<Dictionary<string, string[]>>(json);

.NET Core/System.Text.Json: Enumerate and add/replace json properties/values

Preliminaries

I'll be heavily working with the existing code from my answer to the linked question: .Net Core 3.0 JsonSerializer populate existing object.

As I mentioned, the code for shallow copies works and produces Result 2. So we only need to fix the code for deep copying and get it to produce Result 1.

On my machine the code crashes in PopulateObject when the propertyType is typeof(string), since string is neither a value type nor something represented by an object in JSON. I fixed that back in the original answer, the if must be:

if (elementType.IsValueType || elementType == typeof(string))

Implementing the new requirements

Okay, so the first issue is recognising whether something is a collection. Currently we look at the type of the property that we want to overwrite to make a decision, so now we will do the same. The logic is as follows:

private static bool IsCollection(Type type) =>
type.GetInterfaces().Any(x => x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(ICollection<>));

So the only things we consider collections are things that implement ICollection<T> for some T. We will handle collections completely separately by implementing a new PopulateCollection method. We will also need a way to construct a new collection - maybe the list in the initial object is null, so we need to create a new one before populating it. For that we'll look for its parameterless constructor:

private static object Instantiate(Type type)
{
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Array.Empty<Type>());

if (ctor is null)
{
throw new InvalidOperationException($"Type {type.Name} has no parameterless constructor.");
}

return ctor.Invoke(Array.Empty<object?>());
}

We allow it to be private, because why not.

Now we make some changes to OverwriteProperty:

    private static void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);

if (propertyInfo == null)
{
return;
}

if (updatedProperty.Value.ValueKind == JsonValueKind.Null)
{
propertyInfo.SetValue(target, null);
return;
}

var propertyType = propertyInfo.PropertyType;
object? parsedValue;

if (propertyType.IsValueType || propertyType == typeof(string))
{
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else if (IsCollection(propertyType))
{
var elementType = propertyType.GenericTypeArguments[0];
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);

PopulateCollection(parsedValue, updatedProperty.Value.GetRawText(), elementType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);

PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}

propertyInfo.SetValue(target, parsedValue);
}

The big change is the second branch of the if statement. We find out the type of the elements in the collection and extract the existing collection from the object. If it is null, we create a new, empty one. Then we call the new method to populate it.

The PopulateCollection method will be very similar to OverwriteProperty.

private static void PopulateCollection(object target, string jsonSource, Type elementType)

First we get the Add method of the collection:

var addMethod = target.GetType().GetMethod("Add", new[] { elementType });

Here we expect an actual JSON array, so it's time to enumerate it. For every element in the array we need to do the same thing as in OverwriteProperty, depending on whether we have a value, array or object we have different flows.

foreach (var property in json.EnumerateArray())
{
object? element;

if (elementType.IsValueType || elementType == typeof(string))
{
element = JsonSerializer.Deserialize(jsonSource, elementType);
}
else if (IsCollection(elementType))
{
var nestedElementType = elementType.GenericTypeArguments[0];
element = Instantiate(elementType);

PopulateCollection(element, property.GetRawText(), nestedElementType);
}
else
{
element = Instantiate(elementType);

PopulateObject(element, property.GetRawText(), elementType);
}

addMethod.Invoke(target, new[] { element });
}

Uniqueness

Now we have an issue. The current implementation will always add to the collection, regardless of its current contents. So the thing this would return is neither Result 1 nor Result 2, it'd be Result 3:

{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/indexnews"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More news",
"Link": "/morenews"
}
]
}

We had the array with links 10 and 11 and then added another one with links 11 and 12. There is no obvious natural way of dealing with this. The design decision I chose here is: the collection decides whether the element is already there. We will call the default Contains method on the collection and add if and only if it returns false. It requires us to override the Equals method on Links to compare the Id:

public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;

public override int GetHashCode() => Id.GetHashCode();

Now the changes required are:

  • First, fetch the Contains method:
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
  • Then, check it after we get an element:
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
addMethod.Invoke(target, new[] { element });
}

Tests

I add a few things to your Pages and Links class, first of all I override ToString so we can easily check our results. Then, as mentioned, I override Equals for Links:

public class Pages
{
public string Title { get; set; }
public string Head { get; set; }
public List<Links> Links { get; set; }

public override string ToString() =>
$"Pages {{ Title = {Title}, Head = {Head}, Links = {string.Join(", ", Links)} }}";
}

public class Links
{
public int Id { get; set; }
public string Text { get; set; }
public string Link { get; set; }

public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;

public override int GetHashCode() => Id.GetHashCode();

public override string ToString() => $"Links {{ Id = {Id}, Text = {Text}, Link = {Link} }}";
}

And the test:

var initial = @"{
""Title"": ""Startpage"",
""Links"": [
{
""Id"": 10,
""Text"": ""Start"",
""Link"": ""/index""
},
{
""Id"": 11,
""Text"": ""Info"",
""Link"": ""/info""
}
]
}";

var update = @"{
""Head"": ""Latest news"",
""Links"".


Related Topics



Leave a reply



Submit