Skip to content

Instantly share code, notes, and snippets.

@jasonswearingen
Last active October 9, 2024 03:34
Show Gist options
  • Save jasonswearingen/3394f32a8a962873cbf911b95d241584 to your computer and use it in GitHub Desktop.
Save jasonswearingen/3394f32a8a962873cbf911b95d241584 to your computer and use it in GitHub Desktop.
snippet to deserialize json5 using system.text.json
//this is a snippet from my framework. you'll have to make some tiny syntax adjustments to use this.
//for details on json5, see https://json5.org/
/// <summary>
/// Preprocess a JSON5 string to convert it to valid JSON for Microsoft's System.Text.Json deserializer.
/// </summary>
/// <param name="json5String">The JSON5 string to preprocess.</param>
/// <returns>A valid JSON string.</returns>
private static string PreprocessJson5ToJson(string json5String)
{
// Handle multi-line strings with backslash-newline
json5String = @"\\\r?\n\s*"._ToRegex().Replace(json5String, "");
// Convert unquoted keys to quoted keys (supports ECMAScript 5.1 IdentifierName)
json5String = @"(^|[{,\s])([$_\p{L}][$_\p{L}\p{Nd}]*)\s*:"._ToRegex().Replace(json5String, "$1\"$2\":");
// Remove leading plus sign from numbers (simplified version)
json5String = @"([^\w\.\-])\+(\d+(\.\d*)?([eE][+\-]?\d+)?)"._ToRegex().Replace(json5String, "$1$2");
// Add leading zero to numbers starting with decimal point (simplified version)
json5String = @"([^\w\.])\.(\d+([eE][+\-]?\d+)?)"._ToRegex().Replace(json5String, "$10.$2");
// Add trailing zero to numbers ending with decimal point (simplified version without negative lookahead)
json5String = @"(\d+)\.(\s|,|\}|\])"._ToRegex().Replace(json5String, "$1.0$2");
// Convert hexadecimal numbers to decimal
json5String = @"0x[0-9a-fA-F]+"._ToRegex().Replace(json5String, m => Convert.ToInt64(m.Value, 16).ToString());
// Convert single-quoted strings to double-quoted strings, handling escapes (simplified version)
json5String = @"'([^'\\]*(\\.[^'\\]*)*)'"._ToRegex().Replace(json5String, m => "\"" + m.Groups[1].Value.Replace("\"", "\\\"") + "\"");
// Convert NaN, Infinity, -Infinity to strings without positive lookahead. will be converted back to number by our `NumberHandlingConverter` converter in a later step
json5String = @"([:,\s\[\{])\s*(NaN|Infinity|-Infinity)(\s|,|\]|\})"._ToRegex().Replace(json5String, m => m.Groups[1].Value + "\"" + m.Groups[2].Value + "\"" + m.Groups[3].Value);
return json5String;
}
/// <summary>
/// deserialize a json file using json5, which is less strict about json formatting
/// </summary>
/// <typeparam name="TJsonSerialized"></typeparam>
/// <param name="json5ResFilePath"></param>
/// <returns></returns>
public static TJsonSerialized DeserializeJson5<TJsonSerialized>(string json5String)
{
//dotnet, doesn't support unquoted keys
var jsonString = PreprocessJson5ToJson(json5String);
JsonSerializerOptions jsonOptions = new()
{
AllowTrailingCommas = true,
IncludeFields = true,
//MaxDepth = 10,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true,
Converters = { new CaseInsensitiveEnumConverter(), new NumberHandlingConverter() },
//Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy.CamelCase, allowIntegerValues: true) }
};
//var jsonString = FileAccess.Open(match, FileAccess.ModeFlags.Read).GetAsText();
var jsonFile = JsonSerializer.Deserialize<TJsonSerialized>(jsonString, jsonOptions);
return jsonFile;
}
}
/// <summary>
/// Custom converter to handle special number values like Infinity, -Infinity, and NaN.
/// </summary>
internal class NumberHandlingConverter : JsonConverter<double>
{
public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string value = reader.GetString();
return value.ToLower() switch
{
"infinity" => double.PositiveInfinity,
"-infinity" => double.NegativeInfinity,
"nan" => double.NaN,
_ => throw new JsonException($"Unable to convert \"{value}\" to a valid double.")
};
}
else if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetDouble();
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
{
if (double.IsPositiveInfinity(value))
{
writer.WriteStringValue("Infinity");
}
else if (double.IsNegativeInfinity(value))
{
writer.WriteStringValue("-Infinity");
}
else if (double.IsNaN(value))
{
writer.WriteStringValue("NaN");
}
else
{
writer.WriteNumberValue(value);
}
}
}
/// <summary>
/// Custom converter to handle case-insensitive deserialization of enums.
/// </summary>
internal class CaseInsensitiveEnumConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum;
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var converterType = typeof(CaseInsensitiveEnumConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
}
}
internal class CaseInsensitiveEnumConverter<T> : JsonConverter<T> where T : struct, Enum
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string enumValue = reader.GetString();
if (Enum.TryParse(enumValue, ignoreCase: true, out T result))
{
return result;
}
throw new JsonException($"Unable to convert \"{enumValue}\" to Enum \"{typeof(T)}\".");
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment