Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support default values #24

Merged
merged 1 commit into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<ItemGroup>
<ProjectReference Include="../Chickensoft.Serialization/Chickensoft.Serialization.csproj" />

<PackageReference Include="Chickensoft.Introspection" Version="1.5.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="1.5.0" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.Introspection" Version="1.7.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="1.7.0" PrivateAssets="all" OutputItemType="analyzer" />
</ItemGroup>
</Project>
25 changes: 25 additions & 0 deletions Chickensoft.Serialization.Tests/test/fixtures/ModelWithEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Chickensoft.Serialization.Tests.Fixtures;

using System.Text.Json.Serialization;
using Chickensoft.Introspection;

[JsonSerializable(typeof(ModelWithEnum.ModelType))]
public partial class ModelWithEnumContext : JsonSerializerContext;

[Meta, Id("model_with_enum")]
public partial record ModelWithEnum {
[Save("a_type")]
public ModelType AType { get; init; } = ModelType.Basic;

[Save("b_type")]
public ModelType BType { get; init; } = ModelType.Advanced;

[Save("c_type")]
public ModelType CType { get; init; }

public enum ModelType {
Basic,
Advanced,
Complex
}
}
76 changes: 76 additions & 0 deletions Chickensoft.Serialization.Tests/test/src/ModelWithEnumTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace Chickensoft.Serialization.Tests;

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Chickensoft.Serialization.Tests.Fixtures;
using Shouldly;
using Xunit;

public class ModelWithEnumTest {
[Fact]
public void DeserializesAndRespectsDefaultValues() {
var value = /*lang=json,strict*/ """
{
"$type": "model_with_enum",
"$v": 1,
"c_type": "Complex"
}
""";

var options = new JsonSerializerOptions {
WriteIndented = true,
TypeInfoResolver = JsonTypeInfoResolver.Combine(
ModelWithEnumContext.Default,
new SerializableTypeResolver()
),
Converters = {
new JsonStringEnumConverter<ModelWithEnum.ModelType>(),
new SerializableTypeConverter()
},
};

var result = JsonSerializer.Deserialize<ModelWithEnum>(value, options);

result.ShouldNotBeNull();

result.AType.ShouldBe(ModelWithEnum.ModelType.Basic); // default value
result.BType.ShouldBe(ModelWithEnum.ModelType.Advanced); // default value

result.CType.ShouldBe(ModelWithEnum.ModelType.Complex); // from JSON
}

[Fact]
public void DeserializesAndOverridesDefaultValues() {
var value = /*lang=json,strict*/ """
{
"$type": "model_with_enum",
"$v": 1,
"a_type": "Advanced",
"b_type": "Basic"
}
""";

var options = new JsonSerializerOptions {
WriteIndented = true,
TypeInfoResolver = JsonTypeInfoResolver.Combine(
ModelWithEnumContext.Default,
new SerializableTypeResolver()
),
Converters = {
new JsonStringEnumConverter<ModelWithEnum.ModelType>(),
new SerializableTypeConverter()
},
};

var result = JsonSerializer.Deserialize<ModelWithEnum>(value, options);

result.ShouldNotBeNull();

result.AType.ShouldBe(ModelWithEnum.ModelType.Advanced); // default value
result.BType.ShouldBe(ModelWithEnum.ModelType.Basic); // default value

// missing json values should result in whatever is the 0 value of the enum
result.CType.ShouldBe(ModelWithEnum.ModelType.Basic);
}
}
2 changes: 1 addition & 1 deletion Chickensoft.Serialization/Chickensoft.Serialization.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</PackageReference>
<PackageReference Include="Chickensoft.Collections" Version="1.8.5" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<PackageReference Include="Chickensoft.Introspection" Version="1.5.0" />
<PackageReference Include="Chickensoft.Introspection" Version="1.7.0" />
<PackageReference Include="PolyKit" Version="3.0.9" />
</ItemGroup>
</Project>
38 changes: 32 additions & 6 deletions Chickensoft.Serialization/src/SerializableTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ JsonSerializerOptions options
var value = hasInitProps ? null : metadata.Factory();

var initProps = hasInitProps ? new Dictionary<string, object?>() : null;
var normalProps = hasInitProps ? new List<Action>() : null;

foreach (var property in properties) {
if (GetPropertyId(property) is not { } propertyId) {
Expand Down Expand Up @@ -143,12 +144,16 @@ propertyValueJsonNode is JsonObject propertyJsonObj &&
);
}

var shouldSet = isPresentInJson;

if (
!isPresentInJson &&
propertyValue is null &&
IsCollection(property.GenericType.OpenType)
IsCollection(property.GenericType.OpenType) &&
!property.HasDefaultValue
) {
// The value of this collection property is missing from the json.
// Property is not in the json, but it's a collection value that doesn't
// have a default value in the model.
//
// In this scenario, we actually prefer an empty collection. We only
// deserialize a collection to null if it doesn't have a setter or
// if it's present in the json *and* explicitly set to null.
Expand All @@ -166,14 +171,29 @@ propertyValue is null &&
// the collection type by using the callbacks provided in the generated
// introspection data, which is AOT-friendly :D
propertyValue = typeInfo.CreateObject!();

// We want to set the property to the empty collection.
shouldSet = true;
}

if (!shouldSet) {
continue;
}

if (hasInitProps) {
// We'll construct the object later.
initProps!.Add(property.Name, propertyValue);
// Init properties require us to set properties later, so we save
// the init prop values to use in the generated metadata constructor.
//
// We also save closures which will set the normal properties, too.
if (property.IsInit) {
initProps!.Add(property.Name, propertyValue);
}
else if (property.Setter is { } propertySetter) {
normalProps!.Add(() => propertySetter(value!, propertyValue));
}
}
else if (property.Setter is { } propertySetter) {
// We can set the property right away.
// We can set the property immediately since there are no init props.
propertySetter(value!, propertyValue);
}
}
Expand All @@ -182,6 +202,12 @@ propertyValue is null &&
// init properties.
if (hasInitProps) {
value = metadata.Metatype.Construct(initProps);

// Set other properties that are not init properties now that we have
// an object.
foreach (var setProp in normalProps!) {
setProp();
}
}

// Upgrade the deserialized object as needed.
Expand Down