using System; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace GB5Shared.JsonConverter { // Clients occasionally send numeric fields as an empty string (e.g. an // unfilled input on a draft save) instead of omitting the field or // sending 0. JsonNumberHandling.AllowReadingFromString parses a quoted // numeric string fine, but throws JsonException on "" since it can't be // parsed as a number. This converter treats blank/whitespace strings as // default(0) while still accepting real numbers and numeric strings. public class FlexibleNumberConverter : JsonConverter where T : struct { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { case JsonTokenType.Null: return default; case JsonTokenType.String: var raw = reader.GetString(); if (string.IsNullOrWhiteSpace(raw)) return default; return ParseString(raw, typeToConvert); case JsonTokenType.Number: return ReadNumber(ref reader, typeToConvert); default: throw new JsonException($"Unexpected token {reader.TokenType} when parsing {typeToConvert}."); } } private static T ReadNumber(ref Utf8JsonReader reader, Type typeToConvert) { if (typeToConvert == typeof(double)) return (T)(object)reader.GetDouble(); if (typeToConvert == typeof(float)) return (T)(object)reader.GetSingle(); if (typeToConvert == typeof(decimal)) return (T)(object)reader.GetDecimal(); if (typeToConvert == typeof(int)) return (T)(object)reader.GetInt32(); if (typeToConvert == typeof(long)) return (T)(object)reader.GetInt64(); if (typeToConvert == typeof(short)) return (T)(object)reader.GetInt16(); if (typeToConvert == typeof(byte)) return (T)(object)reader.GetByte(); if (typeToConvert == typeof(sbyte)) return (T)(object)reader.GetSByte(); if (typeToConvert == typeof(uint)) return (T)(object)reader.GetUInt32(); if (typeToConvert == typeof(ulong)) return (T)(object)reader.GetUInt64(); if (typeToConvert == typeof(ushort)) return (T)(object)reader.GetUInt16(); throw new NotSupportedException($"Type {typeToConvert} is not supported by FlexibleNumberConverter"); } private static T ParseString(string raw, Type typeToConvert) { if (typeToConvert == typeof(double) && double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) return (T)(object)d; if (typeToConvert == typeof(float) && float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var f)) return (T)(object)f; if (typeToConvert == typeof(decimal) && decimal.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var m)) return (T)(object)m; if (typeToConvert == typeof(int) && int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)) return (T)(object)i; if (typeToConvert == typeof(long) && long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l)) return (T)(object)l; if (typeToConvert == typeof(short) && short.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var s)) return (T)(object)s; if (typeToConvert == typeof(byte) && byte.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b)) return (T)(object)b; if (typeToConvert == typeof(sbyte) && sbyte.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sb)) return (T)(object)sb; if (typeToConvert == typeof(uint) && uint.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ui)) return (T)(object)ui; if (typeToConvert == typeof(ulong) && ulong.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ul)) return (T)(object)ul; if (typeToConvert == typeof(ushort) && ushort.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var us)) return (T)(object)us; throw new JsonException($"Unable to convert \"{raw}\" to {typeToConvert}."); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { writer.WriteRawValue(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0"); } } // Non-generic factory so it can be registered once in JsonSerializerOptions.Converters // and apply to every numeric primitive type encountered, rather than listing each by hand. public class FlexibleNumberConverterFactory : JsonConverterFactory { private static readonly Type[] SupportedTypes = { typeof(double), typeof(float), typeof(decimal), typeof(int), typeof(long), typeof(short), typeof(byte), typeof(sbyte), typeof(uint), typeof(ulong), typeof(ushort) }; public override bool CanConvert(Type typeToConvert) => Array.IndexOf(SupportedTypes, typeToConvert) >= 0; public override System.Text.Json.Serialization.JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var converterType = typeof(FlexibleNumberConverter<>).MakeGenericType(typeToConvert); return (System.Text.Json.Serialization.JsonConverter)Activator.CreateInstance(converterType)!; } } }