分享處理JSON反序列化轉回物件的建構式相關問題。

就拿早先文章提到的Ticker類別當例子:

public class Ticker
{
    readonly string symbol;
    readonly string market;
    public string Symbol { get { return symbol; } }
    public string Market { get { return market; } }
    public string FullSymbol
    {
        get { return Symbol + "." + Market; }
    }
 
    public Ticker(string symbol, string market)
    {
        this.symbol = symbol;
        this.market = market;
    }
    public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
        if (p.Length != 2) throw new ArgumentException();
        this.symbol = p[0];
        this.market = p[1];
    }
}

我們建立一個2330.TW Ticker,以Json.NET序列化成JSON字串,再反序列化回Ticker物件:

        static void Main(string[] args)
        {
            var t1 = new Ticker("2330", "TW");
            var json = JsonConvert.SerializeObject(t1);
            Console.WriteLine("JSON={0}", json);
            try {
                var t2 = JsonConvert.DeserializeObject<Ticker>(json);
                Console.WriteLine("Restored={0}", t2.FullSymbol);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error: {0}", ex.Message);
            }
 
            Console.Read();
        }

觸礁了~

JSON={"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}
Error: Unable to find a constructor to use for type OpTest.Ticker.
A class should either have a default constructor, one constructor with arguments or a
constructor marked with the JsonConstructor attribute. Path 'Symbol', line 1, position 10.

由於Ticker有兩個建構式,Json.NET在反序化時無法決定該用哪一個。

當物件有預設建構式(不傳任何參數),Json.NET會先以預設建構式建立物件,再一一設定屬性值。若遇到屬性值只能由物件內部指定(例如:public string Prop { get; private set; })或像Ticker使用readonly欄位,屬性只能透過建構式設定的狀況,類別不一定有預設建構式可用。

Json.NET很聰明,即使是帶參數的建構式,也會試著將JSON裡的屬性做為參數傳。例如:當JSON為{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}而建構式為public Ticker(string symbol, string market),Json.NET會偵測參數名稱symbol、market,在JSON屬性中尋找相同名稱的屬性值(忽略大小寫),找不到則填入參數型別預設值,一樣可以建構物件。

在Ticker案例問題則出在有兩個建構式,Json.NET無法判斷要用哪一個:
public Ticker(string symbol, string market)
public Ticker(string fullSymbol)

所幸,威力強大如Json.NET,當然已料想到這類情境,提供了JsonConstructorAttribute,我們只需在其中一個建構式加上[JsonConstructor]即可解決問題:

        [JsonConstructor]
        public Ticker(string symbol, string market)
        {
            this.symbol = symbol;
            this.market = market;
        }

最後,來對程式進行優化,順便展現Json.NET的彈性。

大家有沒有發現Ticker物件轉出的JSON字串包含Symbol、Market、FullSymbol,資料重複性太高?
{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}

嚴格來說,只要留FullSymbol就好,用JsonIgnoreAttribute排除Symbol與Market。另外,FullSymbol有點囉嗦,
我們用JsonProperty("Tick")幫它改個簡短的名字:

public class Ticker
{
    readonly string symbol;
    readonly string market;
 
    [JsonIgnore]
    public string Symbol { get { return symbol; } }
    [JsonIgnore]
    public string Market { get { return market; } }
    public Ticker(string symbol, string market)
    {
        this.symbol = symbol;
        this.market = market;
    }
    [JsonConstructor]
    public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
        if (p.Length != 2) throw new ArgumentException();
        this.symbol = p[0];
        this.market = p[1];
    }
    [JsonProperty("Tick")]
    public string FullSymbol
    {
        get
        {
            return Symbol + "." + Market;
        }
    }
}

修改後的JSON樣式及測試結果如下:

JSON={"Tick":"2330.TW"}
Restored=2330.TW

JSON的長度由54個字元

{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}

縮短成18個字元

{"Tick":"2330.TW"}

體積縮小到1/3,這招在資料筆數量多或對傳輸量斤斤計較的場合很管用,提供大家參考。

感想:Json.NET並不是檯面上效能最好的JSON程式庫,但功能完整性及成熟度實在讓人無話可說,還是老話一句,Json.NET真該納入.NET核心的。

好了,又到了呼口號時間:

Json.NET好威呀!


Comments

# by guest

正好遇到這個問題,感謝你的解答

Post a comment