kewur
kewur

Reputation: 491

System.Text.Json custom null values

I'm using an api endpoint and they are returning invalid responses in some of the fields instead of null values for decimals. here is an example

{
  "name": "MyName",
  "decimalOne": "0.01",
  "decimalTwo": "None",
  "decimalThree": "-"
}

I would like my code to handle these custom "null" things they are sending me as just regular nulls. Is there a way for me to do this?

Upvotes: 0

Views: 1526

Answers (2)

kewur
kewur

Reputation: 491

I've decided to do a NullableConverterFactory with additional "null" parameters we can pass in.

looks like this.

/// <summary>
/// Converter that can parse nullable types. int? decimal? etc
/// </summary>
public class NullableConverterFactory : JsonConverterFactory
{
    private static readonly byte [] EMPTY = Array.Empty<byte>();

    private static readonly HashSet<byte[]> NULL_VALUES = new()
                                                          {
                                                              EMPTY
                                                          };

    /// <summary>
    /// empty constructor.
    /// </summary>
    public NullableConverterFactory()
    {
    }

    /// <summary>
    /// Supply additional accepted null values as byte[] here.
    /// </summary>
    /// <param name="additionalNullValues"></param>
    public NullableConverterFactory(HashSet<byte[]> additionalNullValues)
    {
        foreach (byte[] nullValue in additionalNullValues)
        {
            NULL_VALUES.Add(nullValue);
        }
    }

    /// <summary>
    /// Returns true if this converter can convert the value.
    /// </summary>
    /// <param name="typeToConvert"></param>
    /// <returns></returns>
    public override bool CanConvert(Type typeToConvert) => Nullable.GetUnderlyingType(typeToConvert) != null;

    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => 
        (JsonConverter)Activator.CreateInstance(
                                                typeof(NullableConverter<>).MakeGenericType(
                                                                                            new Type[] { Nullable.GetUnderlyingType(type) }),
                                                BindingFlags.Instance | BindingFlags.Public,
                                                binder: null,
                                                args: new object[] { options, NULL_VALUES },
                                                culture: null);

    class NullableConverter<T> : JsonConverter<T?> where T : struct
    {
        private readonly HashSet<byte[]> _nullValues;

        // DO NOT CACHE the return of (JsonConverter<T>)options.GetConverter(typeof(T)) as DoubleConverter.Read() and DoubleConverter.Write()
        // DO NOT WORK for nondefault values of JsonSerializerOptions.NumberHandling which was introduced in .NET 5
        public NullableConverter(JsonSerializerOptions options, HashSet<byte[]> nullValues)
        {
            _nullValues = nullValues;
        }

        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.String)
            {
                return JsonSerializer.Deserialize<T>(ref reader, options);
            }

            foreach (byte[] nullValue in _nullValues)
            {
                if (reader.ValueTextEquals(nullValue))
                {
                    return null;
                }
            }

            return JsonSerializer.Deserialize<T>(ref reader, options);
        }           

        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, value.Value, options);
    }
}

usage:

new JsonSerializerOptions()
          {
              PropertyNameCaseInsensitive = true,
              Converters =
              {
                  new NullableConverterFactory(new HashSet<byte[]>
                                               {
                                                   Encoding.UTF8.GetBytes("-"),
                                                   Encoding.UTF8.GetBytes("None")
                                               })
              },
              NumberHandling      = JsonNumberHandling.AllowReadingFromString,
              AllowTrailingCommas = true
          };

Upvotes: 0

Serge
Serge

Reputation: 43969

you can try something like this, ( I don't know how many different decimal fields you have)

Name name=System.Text.Json.JsonSerializer.Deserialize<Name>(json);

public class Name
{
    public string name {get; set;}
    
    [JsonPropertyName("decimalOne")]
    public string decimalOneString  { get; set;}
    
    [System.Text.Json.Serialization.JsonIgnore]
    public decimal? decimalOne
    {
        get { if (decimal.TryParse(decimalOneString, out var result)) return result;
        return null;
        }
        set { decimalOneString = value.ToString();}
    }
}

Upvotes: 0

Related Questions