Contents

如何在 .Net 中處理 JSON 轉換時的過濾特殊字串

最近在專案中串接 OpenData 時,遇到了一個問題。某些資料中出現了一些奇怪的符號?,這在資料類型為 float 時會導致程式出錯。在這篇文章中,我將分享我找到的解決這個問題的方法。

一般 Trim 方法過濾一些符號

參考來源: Trim String in C#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// String with whitespaces
string hello = " hello C# Corner has white spaces ";
// Remove whitespaces from both ends
Console.WriteLine(hello.Trim());

// String with characters
string someString = ".....My name is Mahesh Chand****";
char[] charsToTrim = { '*', '.' };
string cleanString = someString.Trim(charsToTrim);
Console.WriteLine(cleanString);

public string Trim (params char[]? trimChars); 從官方文件中我們可以看到,Trim 方法可以接受多個參數。由於 params 的關係,我們可以直接將要過濾的字符作為參數傳入,如 someString.Trim('*', '.');

Trim 方法會從字串的開頭和結尾過濾掉指定的字符。在上面的範例中,Trim 方法過濾掉了變數 someString 開頭的 ‘.’ 字符和結尾的 ‘*’ 字符,得到了乾淨的字串 cleanString

更多關於 Trim 方法的資訊,可以參考 https://learn.microsoft.com/zh-tw/dotnet/api/system.string.trim?view=net-5.0

Json.NET (Newtonsoft) 屬性 Trim 欄位

在處理 JSON 字串的反序列化時,我們可能會遇到一些字串前後有多餘空白的情況。這時,我們可以使用 Json.NET (Newtonsoft) 的 TrimmingConverter 來自動修剪這些空白。以下是一個 TrimmingConverter 的範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class TrimmingConverter : JsonConverter
{
    public override bool CanRead => true;
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => objectType == typeof(string);

    public override object ReadJson(JsonReader reader, Type objectType,
                                    object existingValue, JsonSerializer serializer)
    {
        return ((string)reader.Value)?.Trim();
    }

    public override void WriteJson(JsonWriter writer, object value, 
                                   JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

在反序列化過程中,TrimmingConverter 會自動修剪字串的前後空白。以下是一個使用 TrimmingConverter 的範例:

1
2
3
4
var json = @"{ name:"" John "" }"
var p = JsonConvert.DeserializeObject<Person>(json, new TrimmingConverter());
Console.WriteLine("Name is: \"{0}\"", p.Name);
//Name is: "John"

我們也可以在類別的屬性上使用 JsonConverter 屬性來指定 TrimmingConverter,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Person
{
    [JsonProperty("name")]
    [JsonConverter(typeof(TrimmingConverter))] // <-- that's the important line
    public string Name { get; set; }
    [JsonProperty("other")]
    public string Other { get; set; }
}

var json = @"{ name:"" John "", other:"" blah blah blah "" }"
var p = JsonConvert.DeserializeObject<Person>(json);
Console.WriteLine("Name is: \"{0}\"", p.Name);
Console.WriteLine("Other is: \"{0}\"", p.Other);

//Name is: "John"
//Other is: " blah blah blah "

LinqPad範例

更多關於 Json.NET 的 JsonConverter 類別的資訊,可以參考:

System.Text.Json 用屬性 Trim 方法

Warning
我進行的實驗發現,使用 Json.NET 可以正常解析浮點數字串 "122.456"float,但是使用 System.Text.Json 則會發生錯誤。這顯示 System.Text.Json 在處理數據時會比較嚴謹。在程式上改寫時,我們需要特別注意這一點。ChatGPT 提供了一種解決方案,即使用 JsonConvert 來解決這個問題。
參考: ChatGPT 問答
Info
這邊我使用了 Json.Net 進行轉換。目前台灣的 OpenData 大部分都是從 csv 轉出的 json,因此大部分的數字都會帶有 " 符號。在這種情況下,使用 Json.Net 進行轉換可能會比較方便。
詳細可以看: 週三、週日廚餘專用限時收受點 | Opendata platform
有時候資料中會包含一些? 之類的字元,處理起來可能會比較麻煩。

在 Stack Overflow 上也有一篇相關的討論:c# - Deserialize json with auto-trimming strings - Stack Overflow

以下是一個使用 TrimmingConverter 的程式碼範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var json = @"{ ""Name"":"" John "", ""Other"":"" blah blah blah "" }";
var p = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine("name is: \"{0}\"", p.Name);
Console.WriteLine("Other is: \"{0}\"", p.Other);
public class Person
{
	[JsonConverter(typeof(TrimmingConverter))] // <-- that's the important line
	public string Name { get; set; }
	public string Other { get; set; }
}

public class TrimmingConverter : JsonConverter<string>
{
	/// <summary>
	/// Trim the input string
	/// </summary>
	/// <param name="reader">reader</param>
	/// <param name="typeToConvert">Object type</param>
	/// <param name="options">Existing Value</param>
	/// <returns></returns>
	public override string Read(
		ref Utf8JsonReader reader,
		Type typeToConvert,
		JsonSerializerOptions options) => reader.GetString()?.Trim();

	/// <summary>
	/// Trim the output string
	/// </summary>
	/// <param name="writer">Writer</param>
	/// <param name="value">value</param>
	/// <param name="options">serializer</param>
	public override void Write(
		Utf8JsonWriter writer,
		string value,
		JsonSerializerOptions options) => writer.WriteStringValue(value?.Trim());
}

LinqPad範例
在這個程式碼範例中,TrimmingConverter 會在反序列化過程中自動修剪字串的前後空白。這對於處理帶有前後空白的 JSON 字串非常有用。

Info

System.Text.Json 針對 Json 所有屬性做 JsonConverter 也是有的。

1
2
3
4
5
6
7
8
var options = new JsonSerializerOptions();
options.Converters.Add(new TrimmingConverter());

var json = @"{ ""Name"":"" John "", ""Other"":"" blah blah blah "" }";
var p = JsonSerializer.Deserialize<Person>(json, options);

Console.WriteLine("name is: \"{0}\"", p.Name);
Console.WriteLine("Other is: \"{0}\"", p.Other);

LinqPad範例

我後來選擇 Json.Net 處理過濾特殊字元範例

在處理浮點數字串的轉換時,Json.Net 和 System.Text.Json 的行為有所不同。具體來說,Json.Net 可以自動轉換浮點數字串,而 System.Text.Json 則會報錯。這是由於 System.Text.Json 在處理數字時,對於數字格式的要求比 Json.Net 更嚴格。因此,在這個專案中,我選擇使用 Json.Net 套件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    public class TrimmingFloatConverter : JsonConverter
    {
        public override bool CanRead => true;
        public override bool CanWrite => false;

        public override bool CanConvert(Type objectType) => objectType == typeof(string);

        public override object ReadJson(JsonReader reader, Type objectType,
                                        object existingValue, JsonSerializer serializer)
        {
            return ((string)reader.Value)?.Trim('?', '*', '.', ' ').ToFloat();
        }

        public override void WriteJson(JsonWriter writer, object value,
                                       JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

在上面的程式碼範例中,TrimmingFloatConverter 會在反序列化過程中過濾掉字串中的 ‘?’, ‘*’, ‘.’, ’ ’ 字符,並將過濾後的字串轉換成浮點數。這對於處理帶有特定字符的 JSON 字串非常有用。

Info
我發現 Json.Net 和原生的 System.Text.Json 在處理 JSON 數據時有一些不同,例如在轉換 DateTime 字串格式和將數字字串轉換成數字型別的嚴謹度上。這是我們在選擇使用哪種 JSON 處理庫時需要考慮的因素。
Info
由於 System.Text.Json 的嚴謹度較高,我們無法像在 Json.Net 中那樣直接使用 TrimmingFloatConverter 來過濾和轉換字串。這是我們在使用 System.Text.Json 時需要注意的一點。
Tip

當然 System.Text.Json 也是有對應處理方法,可以參考下面的範例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var options = new JsonSerializerOptions();
options.Converters.Add(new FloatStringConverter());

var person = new Person { Name = "John", Height = 1.8f };
var json = @"{""Name"":""John  "",""Height"":""  1.8   ""}";
json.Dump();
var deserializedPerson = JsonSerializer.Deserialize<Person>(json, options);

deserializedPerson.Dump();

public class FloatStringConverter : JsonConverter<float>
{
    public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetString();
        return float.Parse(value, CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
	}
}


public class Person
{
	public string Name { get; set; }
	public float Height { get; set; }
}

LinqPad範例

彩蛋

Trim strings in JSON paylod

[小菜一碟] Trim () 不只能修剪空白字元而已 | 軟體主廚的程式料理廚房 - 點部落