Skip to content
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,25 @@ documents[1]["Warning"] // #=> "A slightly different error message."
documents[2]["Fatal"] // #=> "Unknown variable \"bar\""
```

#### DefaultIgnoreCondition

You can control whether null or default-valued properties are omitted during serialization using `YamlSerializerOptions.DefaultIgnoreCondition`.

```cs
var options = YamlSerializerOptions.Standard;

// Omit properties that are null
options.DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull;

// Omit properties that are null or have their default value (0, false, etc.)
options.DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingDefault;
```

List of possible values:
- `YamlIgnoreCondition.Never` (default) — Always serialize all properties
- `YamlIgnoreCondition.WhenWritingNull` — Omit properties whose value is `null` (reference types and `Nullable<T>`)
- `YamlIgnoreCondition.WhenWritingDefault` — Omit properties whose value is `null` or the default value for value types (`0`, `false`, `'\0'`, etc.)

#### Naming convention

:exclamation: By default, VYaml maps C# property names in lower camel case (e.g. `propertyName`) format to yaml keys.
Expand Down
10 changes: 9 additions & 1 deletion VYaml.SourceGenerator.Roslyn3/VYamlSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace VYaml.SourceGenerator;

Expand Down Expand Up @@ -319,6 +319,8 @@ static bool TryEmitSerializeMethod(TypeMeta typeMeta, CodeWriter codeWriter, in
codeWriter.AppendLine("emitter.BeginMapping();");
foreach (var memberMeta in memberMetas)
{
var ignoreScope = Emitter.EmitIgnoreConditionCheck(codeWriter, memberMeta);

if (memberMeta.HasKeyNameAlias)
{
codeWriter.AppendLine($"emitter.WriteString(\"{memberMeta.KeyName}\");");
Expand All @@ -328,6 +330,12 @@ static bool TryEmitSerializeMethod(TypeMeta typeMeta, CodeWriter codeWriter, in
codeWriter.AppendLine($"emitter.WriteString(\"{memberMeta.KeyName}\", ScalarStyle.Plain);");
}
codeWriter.AppendLine($"context.Serialize(ref emitter, value.{memberMeta.Name});");

if (ignoreScope != null)
{
ignoreScope.Dispose();
codeWriter.AppendLine("}");
}
}
codeWriter.AppendLine("emitter.EndMapping();");

Expand Down
61 changes: 60 additions & 1 deletion VYaml.SourceGenerator/Emitter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -286,6 +286,8 @@ static bool TryEmitSerializeMethod(TypeMeta typeMeta, CodeWriter codeWriter, in
codeWriter.AppendLine("emitter.BeginMapping();");
foreach (var memberMeta in memberMetas)
{
var ignoreScope = EmitIgnoreConditionCheck(codeWriter, memberMeta);

if (memberMeta.HasKeyNameAlias || typeMeta.NamingConventionByType != NamingConvention.LowerCamelCase)
{
codeWriter.AppendLine($"emitter.WriteString(\"{memberMeta.KeyName}\");");
Expand All @@ -303,12 +305,69 @@ static bool TryEmitSerializeMethod(TypeMeta typeMeta, CodeWriter codeWriter, in
}
}
codeWriter.AppendLine($"context.Serialize(ref emitter, value.{memberMeta.Name});");

if (ignoreScope != null)
{
ignoreScope.Dispose();
codeWriter.AppendLine("}");
}
}
codeWriter.AppendLine("emitter.EndMapping();");

return true;
}

public static IDisposable? EmitIgnoreConditionCheck(CodeWriter codeWriter, MemberMeta memberMeta)
{
var isReferenceType = memberMeta.MemberType.IsReferenceType;
var namedType = memberMeta.MemberType as INamedTypeSymbol;
var isNullableValueType = namedType is { IsGenericType: true } &&
namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T;

if (isReferenceType || isNullableValueType)
{
codeWriter.AppendLine(
$"if (context.Options.DefaultIgnoreCondition == global::VYaml.Serialization.YamlIgnoreCondition.Never || " +
$"value.{memberMeta.Name} != null)");
codeWriter.AppendLine("{");
return codeWriter.BeginIndentScope();
}

if (memberMeta.MemberType.IsValueType)
{
var comparison = GetDefaultValueComparison(memberMeta.MemberType, memberMeta.Name);
codeWriter.AppendLine(
$"if (context.Options.DefaultIgnoreCondition != global::VYaml.Serialization.YamlIgnoreCondition.WhenWritingDefault || " +
$"{comparison})");
codeWriter.AppendLine("{");
return codeWriter.BeginIndentScope();
}

return null;
}

static string GetDefaultValueComparison(ITypeSymbol type, string memberName)
{
var typeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
return typeName switch
{
"bool" or "global::System.Boolean" => $"value.{memberName} != false",
"byte" or "global::System.Byte" => $"value.{memberName} != 0",
"sbyte" or "global::System.SByte" => $"value.{memberName} != 0",
"short" or "global::System.Int16" => $"value.{memberName} != 0",
"ushort" or "global::System.UInt16" => $"value.{memberName} != 0",
"int" or "global::System.Int32" => $"value.{memberName} != 0",
"uint" or "global::System.UInt32" => $"value.{memberName} != 0u",
"long" or "global::System.Int64" => $"value.{memberName} != 0L",
"ulong" or "global::System.UInt64" => $"value.{memberName} != 0UL",
"float" or "global::System.Single" => $"value.{memberName} != 0f",
"double" or "global::System.Double" => $"value.{memberName} != 0d",
"decimal" or "global::System.Decimal" => $"value.{memberName} != 0m",
"char" or "global::System.Char" => $"value.{memberName} != '\\0'",
_ => $"!value.{memberName}.Equals(default({typeName}))"
};
}

static bool TryEmitSerializeMethodUnion(TypeMeta typeMeta, CodeWriter codeWriter, in SourceProductionContext context)
{
var returnType = typeMeta.Symbol.IsValueType
Expand Down
133 changes: 132 additions & 1 deletion VYaml.Tests/Serialization/GeneratedFormatterTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NUnit.Framework;
using NUnit.Framework;
using System;
using VYaml.Annotations;
using VYaml.Serialization;
Expand Down Expand Up @@ -557,5 +557,136 @@ public void Serialize_PrivateMembersSettable()
Assert.That(serialized, Does.Contain("secret: 42"));
}

[Test]
public void Serialize_DefaultIgnoreCondition_Never()
{
var options = new YamlSerializerOptions
{
Resolver = StandardResolver.Instance,
DefaultIgnoreCondition = YamlIgnoreCondition.Never,
};

var obj = new TypeWithNullableProperties();
var result = Serialize(obj, options);

Assert.That(result, Does.Contain("nullableString:"));
Assert.That(result, Does.Contain("nonNullableString:"));
Assert.That(result, Does.Contain("nullableInt:"));
Assert.That(result, Does.Contain("nonNullableInt:"));
Assert.That(result, Does.Contain("boolValue:"));
Assert.That(result, Does.Contain("doubleValue:"));
Assert.That(result, Does.Contain("nullableObject:"));
Assert.That(result, Does.Contain("nonNullableObject:"));
}

[Test]
public void Serialize_DefaultIgnoreCondition_WhenWritingNull()
{
var options = new YamlSerializerOptions
{
Resolver = StandardResolver.Instance,
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull,
};

var obj = new TypeWithNullableProperties
{
NullableString = null,
NullableInt = null,
NullableObject = null,
NonNullableInt = 0,
BoolValue = false,
DoubleValue = 0.0,
};
var result = Serialize(obj, options);

Assert.That(result, Does.Not.Contain("nullableString:"));
Assert.That(result, Does.Contain("nonNullableString:"));
Assert.That(result, Does.Not.Contain("nullableInt:"));
Assert.That(result, Does.Contain("nonNullableInt:"));
Assert.That(result, Does.Contain("boolValue:"));
Assert.That(result, Does.Contain("doubleValue:"));
Assert.That(result, Does.Not.Contain("nullableObject:"));
Assert.That(result, Does.Contain("nonNullableObject:"));
}

[Test]
public void Serialize_DefaultIgnoreCondition_WhenWritingNull_WithValues()
{
var options = new YamlSerializerOptions
{
Resolver = StandardResolver.Instance,
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull,
};

var obj = new TypeWithNullableProperties
{
NullableString = "hello",
NullableInt = 42,
NullableObject = new SimpleTypeOne { One = 1 },
};
var result = Serialize(obj, options);

Assert.That(result, Does.Contain("nullableString:"));
Assert.That(result, Does.Contain("nullableInt:"));
Assert.That(result, Does.Contain("nullableObject:"));
}

[Test]
public void Serialize_DefaultIgnoreCondition_WhenWritingDefault()
{
var options = new YamlSerializerOptions
{
Resolver = StandardResolver.Instance,
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingDefault,
};

var obj = new TypeWithNullableProperties
{
NullableString = null,
NonNullableString = "default",
NullableInt = null,
NonNullableInt = 0,
BoolValue = false,
DoubleValue = 0.0,
NullableObject = null,
};
var result = Serialize(obj, options);

Assert.That(result, Does.Not.Contain("nullableString:"));
Assert.That(result, Does.Contain("nonNullableString:"));
Assert.That(result, Does.Not.Contain("nullableInt:"));
Assert.That(result, Does.Not.Contain("nonNullableInt:"));
Assert.That(result, Does.Not.Contain("boolValue:"));
Assert.That(result, Does.Not.Contain("doubleValue:"));
Assert.That(result, Does.Not.Contain("nullableObject:"));
Assert.That(result, Does.Contain("nonNullableObject:"));
}

[Test]
public void Serialize_DefaultIgnoreCondition_WhenWritingDefault_WithValues()
{
var options = new YamlSerializerOptions
{
Resolver = StandardResolver.Instance,
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingDefault,
};

var obj = new TypeWithNullableProperties
{
NullableString = "hello",
NonNullableInt = 42,
BoolValue = true,
DoubleValue = 3.14,
NullableInt = 10,
};
var result = Serialize(obj, options);

Assert.That(result, Does.Contain("nullableString:"));
Assert.That(result, Does.Contain("nonNullableInt:"));
Assert.That(result, Does.Contain("boolValue:"));
Assert.That(result, Does.Contain("doubleValue:"));
Assert.That(result, Does.Contain("nullableInt:"));
}

}
}
15 changes: 14 additions & 1 deletion VYaml.Tests/TypeDeclarations/Simple.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.Serialization;
using VYaml.Annotations;

Expand Down Expand Up @@ -377,6 +377,19 @@ public WithPrivateMembersSettable()

public int GetSecret() => _secret;
}

[YamlObject]
public partial class TypeWithNullableProperties
{
public string? NullableString { get; set; }
public string NonNullableString { get; set; } = "default";
public int? NullableInt { get; set; }
public int NonNullableInt { get; set; }
public bool BoolValue { get; set; }
public double DoubleValue { get; set; }
public SimpleTypeOne? NullableObject { get; set; }
public SimpleTypeOne NonNullableObject { get; set; } = new SimpleTypeOne();
}
}

// another namespace, same type name
Expand Down
25 changes: 25 additions & 0 deletions VYaml/Serialization/YamlIgnoreCondition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace VYaml.Serialization
{
/// <summary>
/// Specifies the condition under which properties are ignored during serialization.
/// </summary>
public enum YamlIgnoreCondition
{
/// <summary>
/// Property is never ignored during serialization.
/// </summary>
Never = 0,

/// <summary>
/// Property is ignored when its value is null.
/// </summary>
WhenWritingNull = 1,

/// <summary>
/// Property is ignored when its value is the default for its type.
/// For reference types and nullable value types, the default is null.
/// For non-nullable value types, the default is the natural default (0 for numeric types, false for bool, etc.).
/// </summary>
WhenWritingDefault = 2,
}
}
1 change: 1 addition & 0 deletions VYaml/Serialization/YamlSerializerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public class YamlSerializerOptions
public IYamlFormatterResolver Resolver { get; set; } = StandardResolver.Instance;
public NamingConvention NamingConvention { get; set; } = DefaultNamingConvention;
public YamlEmitOptions EmitOptions { get; set; } = new();
public YamlIgnoreCondition DefaultIgnoreCondition { get; set; } = YamlIgnoreCondition.Never;
}
}
Loading