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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ private static List<KeyValuePair<string, object>> FlattenParamsOptions(
// reference types), so skip those to avoid encoding them in the request.
if (value == null)
{
// If this is an emptyable property that was explicitly set to null,
// encode it as an empty string to clear the field on the server.
if (options is IHasSetTracking tracked && tracked.IsPropertySet(property.Name))
{
string newPrefixForNull = NewPrefix(key, keyPrefix);
flatParams.Add(new KeyValuePair<string, object>(newPrefixForNull, string.Empty));
}

continue;
}

Expand Down
17 changes: 17 additions & 0 deletions src/Stripe.net/Infrastructure/IHasSetTracking.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Stripe.Infrastructure
{
/// <summary>
/// Implemented by options classes that support set-tracking for emptyable fields.
/// Allows encoders to check whether a null property was explicitly set (clear the field)
/// vs never set (omit from the request).
/// </summary>
internal interface IHasSetTracking
{
/// <summary>
/// Returns whether the given property was explicitly set by the caller.
/// </summary>
/// <param name="propertyName">The C# property name to check.</param>
/// <returns>True if the property was explicitly set.</returns>
bool IsPropertySet(string propertyName);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Stripe.Infrastructure
{
using System;
using Newtonsoft.Json;

/// <summary>
/// Newtonsoft converter that serializes any <see cref="StringEnum"/>
/// subclass as its <see cref="StringEnum.Value"/> string.
/// </summary>
internal class NewtonsoftStringEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(StringEnum).IsAssignableFrom(objectType);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
}
else
{
writer.WriteValue(((StringEnum)value).Value);
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException(
$"Deserialization of {objectType.Name} (StringEnum) is not supported.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,17 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
switch (valueToSerialize)
{
case null:
// If this is an emptyable property that was explicitly set to null,
// write null even if the global ignore condition would skip it.
bool forceWriteNull = value is IHasSetTracking tracked
&& tracked.IsPropertySet(property.PropertyInfo.Name);

// Use property-level ignore condition if set, otherwise use global setting
var effectiveIgnoreCondition = property.IgnoreCondition ?? options.DefaultIgnoreCondition;

if (effectiveIgnoreCondition != JsonIgnoreCondition.WhenWritingNull &&
effectiveIgnoreCondition != JsonIgnoreCondition.Always)
if (forceWriteNull ||
(effectiveIgnoreCondition != JsonIgnoreCondition.WhenWritingNull &&
effectiveIgnoreCondition != JsonIgnoreCondition.Always))
{
writer.WritePropertyName(property.JsonPropertyName);
writer.WriteNullValue();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Stripe.Infrastructure
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

/// <summary>
/// Serializes dictionary entries preserving null values. The global
/// <see cref="JsonIgnoreCondition.WhenWritingNull"/> setting in
/// <c>JsonEncodedContent</c> would normally skip null dictionary entries, but for
/// metadata fields on update operations, <c>{"key": null}</c> means "delete this key."
/// Apply this converter per-property via attribute.
/// </summary>
internal class STJNullPreservingDictionaryConverter : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}

return typeToConvert.GetGenericTypeDefinition() == typeof(Dictionary<,>);
}

/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var keyType = typeToConvert.GenericTypeArguments[0];
var valueType = typeToConvert.GenericTypeArguments[1];

var converterType = typeof(InnerConverter<,>).MakeGenericType(keyType, valueType);
return (JsonConverter)Activator.CreateInstance(converterType);
}

private class InnerConverter<TKey, TValue> : JsonConverter<Dictionary<TKey, TValue>>
{
public override Dictionary<TKey, TValue> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// Standard deserialization handles null values fine.
// No recursion risk because this converter is per-property (via attribute),
// not registered globally in the options.
return JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options);
}

public override void Write(
Utf8JsonWriter writer,
Dictionary<TKey, TValue> value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var kvp in value)
{
var key = kvp.Key?.ToString();
if (key == null)
{
continue;
}

writer.WritePropertyName(key);
if (kvp.Value == null)
{
writer.WriteNullValue();
}
else
{
JsonSerializer.Serialize(writer, kvp.Value, options);
}
}

writer.WriteEndObject();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace Stripe.Infrastructure
{
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

/// <summary>
/// STJ converter factory that serializes any <see cref="StringEnum"/>
/// subclass as its <see cref="StringEnum.Value"/> string.
/// </summary>
internal class STJStringEnumConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeof(StringEnum).IsAssignableFrom(typeToConvert);
}

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return (JsonConverter)Activator.CreateInstance(
typeof(STJStringEnumConverterInner<>).MakeGenericType(typeToConvert));
}

private class STJStringEnumConverterInner<T> : JsonConverter<T>
where T : StringEnum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException(
$"Deserialization of {typeToConvert.Name} (StringEnum) is not supported.");
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.Value);
}
}
}
}
}
Loading
Loading