Skip to content

Commit

Permalink
JsonNull wrapper.
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianStehle committed May 10, 2022
1 parent 261ff80 commit e1f0b85
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,27 @@ namespace Squidex.ClientLibrary.Tests
{
public class SerializationTests
{
public sealed class MyClass<T>
public sealed record MyClass<T>
{
[JsonConverter(typeof(InvariantConverter))]
public T Value { get; set; }
}

public sealed class MyCamelClass<T>
public sealed record MyWriteClass<T>
{
[JsonConverter(typeof(InvariantWriteConverter))]
public T Value { get; set; }
}

[KeepCasing]
public sealed class MyPascalClass<T>
public sealed record MyCamelClass<T>
{
public T Value { get; set; }
}

private readonly JsonSerializerSettings settings = new JsonSerializerSettings();

public SerializationTests()
[KeepCasing]
public sealed record MyPascalClass<T>
{
settings.Converters.Add(new InvariantConverter());
public T Value { get; set; }
}

[Fact]
Expand Down Expand Up @@ -153,46 +152,58 @@ public void Should_serialize_invariant()
}

[Fact]
public void Should_serialize_dynamic_properties_with_original_casing()
public void Should_serialize_invariant_jsonnull()
{
var source = new DynamicData
var source = new MyClass<JsonNull<string?>>
{
["Property1"] = new JObject()
Value = "hello"
};

var serialized = source.ToJson();

Assert.Contains("\"Property1\": {}", serialized, StringComparison.Ordinal);
Assert.Contains("\"iv\": \"hello\"", serialized, StringComparison.Ordinal);
}

[Fact]
public void Should_deserialize_invariant()
public void Should_serialize_write_invariant_jsonull()
{
var json = "{ 'value': { 'iv': 'hello'} }";
var source = new MyWriteClass<JsonNull<string?>>
{
Value = "hello"
};

var result = JsonConvert.DeserializeObject<MyClass<string>>(json, settings);
var serialized = source.ToJson();

Assert.Equal("hello", result?.Value);
Assert.Contains("\"iv\": \"hello\"", serialized, StringComparison.Ordinal);
}

[Fact]
public void Should_deserialize_invariant_null_value()
public void Should_serialize_localized_jsonnull()
{
var json = "{ 'value': null }";
var source = new
{
value = new
{
en = new JsonNull<string?>("hello")
}
};

var result = JsonConvert.DeserializeObject<MyClass<string>>(json, settings);
var serialized = source.ToJson();

Assert.Null(result?.Value);
Assert.Contains("\"en\": \"hello\"", serialized, StringComparison.Ordinal);
}

[Fact]
public void Should_deserialize_invariant_empty_value()
public void Should_serialize_dynamic_properties_with_original_casing()
{
var json = "{ 'value': {} }";
var source = new DynamicData
{
["Property1"] = new JObject()
};

var result = JsonConvert.DeserializeObject<MyClass<string>>(json, settings);
var serialized = source.ToJson();

Assert.Null(result?.Value);
Assert.Contains("\"Property1\": {}", serialized, StringComparison.Ordinal);
}

[Fact]
Expand Down Expand Up @@ -220,5 +231,75 @@ public void Should_serialize_with_pascal_case()

Assert.Contains("\"Value\": \"hello\"", serialized, StringComparison.Ordinal);
}

[Fact]
public void Should_deserialize_invariant()
{
var json = "{ 'value': { 'iv': 'hello'} }";

var result = json.FromJson<MyClass<string>>();

Assert.Equal("hello", result?.Value);
}

[Fact]
public void Should_deserialize_invariant_null_value()
{
var json = "{ 'value': null }";

var result = json.FromJson<MyClass<string>>();

Assert.Null(result?.Value);
}

[Fact]
public void Should_deserialize_invariant_empty_value()
{
var json = "{ 'value': {} }";

var result = json.FromJson<MyClass<string>>();

Assert.Null(result?.Value);
}

[Fact]
public void Should_deserialize_invariant_jsonnull()
{
var json = "{ 'value': { 'iv': 'hello'} }";

var result = json.FromJson<MyClass<JsonNull<string>>>();

Assert.Equal("hello", result?.Value.Value);
}

[Fact]
public void Should_deserialize_invariant_jsonnull_ull_value()
{
var json = "{ 'value': null }";

var result = json.FromJson<MyClass<JsonNull<string>>>();

Assert.Null(result?.Value.Value);
}

[Fact]
public void Should_deserialize_invariant_jsonnull_empty_value()
{
var json = "{ 'value': {} }";

var result = json.FromJson<MyClass<JsonNull<string>>>();

Assert.Null(result?.Value.Value);
}

[Fact]
public void Should_deserialize_localized_jsonnull()
{
var json = "{ 'value': { 'en': 'hello'} }";

var result = json.FromJson<MyCamelClass<Dictionary<string, JsonNull<string>>>>();

Assert.Equal("hello", result?.Value["en"].Value);
}
}
}
42 changes: 42 additions & 0 deletions csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/JsonNull.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

#pragma warning disable SA1313 // Parameter names should begin with lower-case letter

namespace Squidex.ClientLibrary
{
/// <summary>
/// A value that can be null.
/// </summary>
/// <typeparam name="T">The actual value.</typeparam>
public record struct JsonNull<T>(T Value)
{
/// <summary>
/// Operator to compare the value to the wrapper.
/// </summary>
/// <param name="value">The value to convert.</param>
public static implicit operator JsonNull<T>(T value)
{
return new JsonNull<T>(value);
}

/// <summary>
/// Operator to compare the wrapper to the actual value.
/// </summary>
/// <param name="wrapper">The wrapper to convert.</param>
public static implicit operator T(JsonNull<T> wrapper)
{
return wrapper.Value;
}

/// <inheritdoc/>
public override readonly string ToString()
{
return Value?.ToString() ?? string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageProjectUrl>https://github.com/Squidex/squidex/</PackageProjectUrl>
<PackageTags>Squidex HeadlessCMS</PackageTags>
<TargetFrameworks>netstandard2.0;netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<Version>8.18.0</Version>
<Version>8.19.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@ namespace Squidex.ClientLibrary.Utils
public class ActorConverter : JsonConverter<Actor>
{
/// <inheritdoc />
public override Actor? ReadJson(JsonReader reader, Type objectType, Actor? existingValue, bool hasExistingValue, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, Actor? value, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
if (value == null)
{
return null;
writer.WriteNull();
return;
}

var s = ((string)reader.Value!).Split(':');

return new Actor { Id = s[1], Type = s[0] };
serializer.Serialize(writer, $"{value.Type}:{value.Id}");
}

/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Actor? value, JsonSerializer serializer)
public override Actor? ReadJson(JsonReader reader, Type objectType, Actor? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (value == null)
if (reader.TokenType == JsonToken.Null)
{
writer.WriteNull();
return;
return null;
}

serializer.Serialize(writer, $"{value.Type}:{value.Id}");
var s = ((string)reader.Value!).Split(':');

return new Actor { Id = s[1], Type = s[0] };
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ static HttpClientExtensions()
{
SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
ContractResolver = new JsonNullContractResolver()
};

SerializerSettings.Converters.Add(new StringEnumConverter());
Expand Down Expand Up @@ -63,6 +63,13 @@ public static string ToJson<T>(this T value)
return json;
}

public static T FromJson<T>(this string value)
{
var json = JsonConvert.DeserializeObject<T>(value, SerializerSettings)!;

return json;
}

public static async Task<T?> ReadAsJsonAsync<T>(this HttpContent content)
{
#if NET5_0_OR_GREATER
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Reflection;

namespace Squidex.ClientLibrary.Utils
{
internal class JsonNullContractResolver : CamelCasePropertyNamesContractResolver
{
/*
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
if (contract.UnderlyingType.IsGenericType && contract.UnderlyingType.GetGenericTypeDefinition() == typeof(JsonNull<>))
{
var converterType = typeof(JsonNullConverter<>).MakeGenericType(contract.UnderlyingType.GetGenericArguments()[0]);
var converterObj = Activator.CreateInstance(converterType) as JsonConverter;
contract.Converter = converterObj;
}
return contract;
}
*/

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);

var objectType = (member as PropertyInfo)?.PropertyType;

if (objectType?.IsGenericType == true && objectType.GetGenericTypeDefinition() == typeof(JsonNull<>))
{
var baseType = typeof(JsonNullConverter<>);

if (property.Converter is InvariantConverter)
{
baseType = typeof(JsonNullInvariantConverter<>);
}
else if (property.Converter is InvariantWriteConverter)
{
baseType = typeof(JsonNullInvariantWriteConverter<>);
}

var converterType = baseType.MakeGenericType(objectType.GetGenericArguments()[0]);
var converterObj = Activator.CreateInstance(converterType) as JsonConverter;

property.Converter = converterObj;
}

return property;
}

protected override JsonConverter? ResolveContractConverter(Type objectType)
{
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(JsonNull<>))
{
var converterType = typeof(JsonNullConverter<>).MakeGenericType(objectType.GetGenericArguments()[0]);
var converterObj = Activator.CreateInstance(converterType) as JsonConverter;

return converterObj;
}

return base.ResolveContractConverter(objectType);
}
}
}
Loading

0 comments on commit e1f0b85

Please sign in to comment.