JSON (JavaScript Object Notation) 的序列化和反序列化在工作中偶尔会用到,特别是在处理网络请求的时候。通常的网站或者API接口会提供JSON数据格式,要将JSON字符串转换为C#中的对象,就要用到序列化和反序列化。

恰好最近有一个需求,需要从某个网站上获取一下Shibor数据,这里涉及到了自定义的JSON序列化,简单记录一下。

需求


程序需要从这个网站上获取SHIBOR数据,如下图:

分析


要获取网页信息,首先要分析网页信息的来源方式,如果是静态网页内容,可以使用HttpClient来获取内容,然后通过 HTML 解析库(如HtmlAgilityPack)提取所需的数据。如果网页内容是动态生成的,即网站的内容是在网页加载完成之后,通过另外的API调用填充,那么就不能直接获取网站内容字符串然后进行解析了。这种情况下有两种策略:如果能够模拟网站发起API请求直接获取数据,那么可以通过HttpClient发起API调用来获取需要的数据,然后进行解析,这种方式简单便捷依赖少。如果不行,通常情况下可能需要校验信息比如需要登录然后获得Token等,那么就要使用一些客户端比如Selenium WebDriver 来模拟浏览器行为,等待页面动态内容加载完成后再提取数据。这种方法比较笨重,需要首先安装相关的NuGet包,万不得已不会用。

要分析网页的数据来源类型,可以通过浏览器里面的“开发者工具”(快捷键F12)来协助分析,首先打开”Element",用鼠标选取页面上的内容,下面会显示相关的HTML标签,比如数据部分的内容如下:

可以看到,核心的数据是在ID为“shiborData”的标签里。接下来切换到"Network"标签,然后刷新页面,可以看到页面的所有数据请求:

一个一个查看,可以看到shibor.html这个页面在加载完成之后,在JavaScript里面,通过AJAX发起了一个Post请求,请求的数据格式为json,请求完成之后,解析返回的数据,然后将数据append到上述id为“shiborData”的标签里。

从上述的分析来看,这个Post请求没有做校验,所以是可以直接在C#里面发起一个HttpClient请求来获取数据。

实现


实现分为两部分,获取数据和序列化。

获取数据


从上述分析可知,只需要发起一个Http Post请求即可获取相应的数据,我们可以对返回的JSON数据进行查看和分析:

static ShiborResponse GetShiborData()
{
	var url = "https://www.shibor.org/r/cms/www/chinamoney/data/shibor/shibor.json";
	var httpClient = new HttpClient();

	try
	{
		// 创建一个空的请求内容(如果 POST 请求需要传递参数,这里可以设置)
		var content = new StringContent("{}", Encoding.UTF8, "application/json");

		var response = httpClient.PostAsync(url, content).Result;
		if (response.IsSuccessStatusCode)
		{
			var responseContent = response.Content.ReadAsStringAsync().Result;

			// 假设返回的数据是 JSON 格式,尝试解析
			var options = new JsonSerializerOptions
			{
				PropertyNameCaseInsensitive = true
			};
			return JsonSerializer.Deserialize<ShiborResponse>(responseContent, options);
		}
	}
	catch (Exception ex)
	{
		Console.WriteLine($"发生错误: {ex.Message}");
	}
	return null;
}

上述代码使用了HttpClient发起了一个异步的Post请求,如果请求成功,它是一个JSON字符串,请求的结果存放在了responseContent里了,内容如下:

{
    "head": {
        "version": "2.0", 
        "provider": "CWAP", 
        "req_code": "", 
        "rep_code": "200", 
        "rep_message": "", 
        "ts": 1744859415143, 
        "producer": ""
    }, 
    "data": {
        "showDateEN": "17/04/2025 11:00", 
        "showDateCN": "2025-04-17 11:00"
    }, 
    "records": [
        {
            "termCode": "O/N", 
            "shibor": "1.6340", 
            "shibIdUpDown": "7.40", 
            "shibIdUpDownNum": -7.4, 
            "termCodePath": "O/N"
        }, 
        {
            "termCode": "1W", 
            "shibor": "1.6520", 
            "shibIdUpDown": "4.40", 
            "shibIdUpDownNum": -4.4, 
            "termCodePath": "1W"
        }, 
        {
            "termCode": "2W", 
            "shibor": "1.7350", 
            "shibIdUpDown": "2.20", 
            "shibIdUpDownNum": -2.2, 
            "termCodePath": "2W"
        }, 
        {
            "termCode": "1M", 
            "shibor": "1.7660", 
            "shibIdUpDown": "0.60", 
            "shibIdUpDownNum": -0.6, 
            "termCodePath": "1M"
        }, 
        {
            "termCode": "3M", 
            "shibor": "1.7670", 
            "shibIdUpDown": "0.50", 
            "shibIdUpDownNum": -0.5, 
            "termCodePath": "3M"
        }, 
        {
            "termCode": "6M", 
            "shibor": "1.7760", 
            "shibIdUpDown": "0.60", 
            "shibIdUpDownNum": -0.6, 
            "termCodePath": "6M"
        }, 
        {
            "termCode": "9M", 
            "shibor": "1.7780", 
            "shibIdUpDown": "0.60", 
            "shibIdUpDownNum": -0.6, 
            "termCodePath": "9M"
        }, 
        {
            "termCode": "1Y", 
            "shibor": "1.7770", 
            "shibIdUpDown": "0.60", 
            "shibIdUpDownNum": -0.6, 
            "termCodePath": "1Y"
        }
    ]
}

现在正常情况下,只需要创建一个类似的C#对象来接收以上数据即可。

序列化


获得了原始的JSON数据后,可以直接按照JSON的格式来准备一个对应的C#数据结构即可。但是有时候,这些JSON的格式结构,与C#类中定义的标准规范可能不同。这时就需要对序列化进行控制。

比如,假设只需要获取返回数据里面的时间showDateCN,以及records里的每条记录,在每条记录里面,只需要termCode和shibor。shibor是一个double类型,并且因为是百分比,所以要在获取的数据上除以100。

上面的JSON字符串的字段名称是JSON里面的规范,而我们希望按照C#的标准来对字段的名称和类型进行重命名,比如 showDateCN 类型应该是日期,名称应该为ShowDate,termCode 应该是一个枚举,名称应该改为TermCode。C#代码的定义如下:

// 定义完整响应类
class ShiborResponse
{
	public ShiborDataInfo Data { get; set; }
	public List<ShiborRecord> Records { get; set; }
}

// 定义期限代码的枚举
enum TermCode
{
	Overnight,
	OneWeek,
	TwoWeeks,
	OneMonth,
	ThreeMonths,
	SixMonths,
	NineMonths,
	OneYear
}

// 定义数据信息类
class ShiborDataInfo
{
	public DateTime ShowDate { get; set; }
}

// 定义记录信息类
class ShiborRecord
{
	public TermCode TermCode { get; set; }
	public double Shibor { get; set; }
}

直接将JSON字符反串序列化为上述的ShiborResponse对象肯定会报错,因为这里面C#定义的字段及类型与返回的JSON不同,并且枚举类型,日期类型,double类型的转换并除以100的这些逻辑都没有指定。这里以使用 System.Text.Json 为例来说明:

首先需要指定JsonProperyName,来将JSON里面的字段映射到C#的字段,然后定义一些转换类:

class ShiborResponse
{
	[JsonPropertyName("data")]
	public ShiborDateInfo Data { get; set; }
	[JsonPropertyName("records")]
	public List<ShiborRecord> Records { get; set; }
}

// 定义期限代码的枚举
enum TermCode
{
	[JsonPropertyName("O/N")]
	Overnight,
	[JsonPropertyName("1W")]
	OneWeek,
	[JsonPropertyName("2W")]
	TwoWeeks,
	[JsonPropertyName("1M")]
	OneMonth,
	[JsonPropertyName("3M")]
	ThreeMonths,
	[JsonPropertyName("6M")]
	SixMonths,
	[JsonPropertyName("9M")]
	NineMonths,
	[JsonPropertyName("1Y")]
	OneYear
}

// 定义数据信息类
class ShiborDateInfo
{
	[JsonPropertyName("showDateCN")]
	[JsonConverter(typeof(CustomDateTimeConverter))]
	public DateTime ShowDate { get; set; }
}

// 定义记录信息类
class ShiborRecord
{
	[JsonConverter(typeof(CustomTermCodeConverter))]
	[JsonPropertyName("termCode")]
	public TermCode TermCode { get; set; }
	[JsonConverter(typeof(CustomShiborConverter))]
	public double Shibor { get; set; }
}

类型转换相关的类如下:

// 自定义日期时间转换器
class CustomDateTimeConverter : JsonConverter<DateTime>
{
	private readonly string[] _formats = { "yyyy-MM-dd HH:mm" };

	public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (DateTime.TryParseExact(reader.GetString(), _formats, null, System.Globalization.DateTimeStyles.None, out DateTime result))
		{
			return result;
		}
		throw new JsonException($"无法将 {reader.GetString()} 转换为 DateTime 类型。");
	}

	public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
	{
		writer.WriteStringValue(value.ToString(_formats[0]));
	}
}

// 自定义枚举转换器
class CustomTermCodeConverter : JsonConverter<TermCode>
{
	public override TermCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		string value = reader.GetString();
		switch (value)
		{
			case "O/N":
				return TermCode.Overnight;
			case "1W":
				return TermCode.OneWeek;
			case "2W":
				return TermCode.TwoWeeks;
			case "1M":
				return TermCode.OneMonth;
			case "3M":
				return TermCode.ThreeMonths;
			case "6M":
				return TermCode.SixMonths;
			case "9M":
				return TermCode.NineMonths;
			case "1Y":
				return TermCode.OneYear;
			default:
				throw new JsonException($"无法将 {value} 转换为 TermCode 枚举类型。");
		}
	}

	public override void Write(Utf8JsonWriter writer, TermCode value, JsonSerializerOptions options)
	{
		string termCodeString;
		switch (value)
		{
			case TermCode.Overnight:
				termCodeString = "O/N";
				break;
			case TermCode.OneWeek:
				termCodeString = "1W";
				break;
			case TermCode.TwoWeeks:
				termCodeString = "2W";
				break;
			case TermCode.OneMonth:
				termCodeString = "1M";
				break;
			case TermCode.ThreeMonths:
				termCodeString = "3M";
				break;
			case TermCode.SixMonths:
				termCodeString = "6M";
				break;
			case TermCode.NineMonths:
				termCodeString = "9M";
				break;
			case TermCode.OneYear:
				termCodeString = "1Y";
				break;
			default:
				throw new JsonException($"无法将 {value} 转换为字符串。");
		}
		writer.WriteStringValue(termCodeString);
	}
}

// 自定义 Shibor 字段转换器
class CustomShiborConverter : JsonConverter<double>
{
	public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		string value = reader.GetString();
		if (double.TryParse(value, out double shiborValue))
		{
			return shiborValue / 100;
		}
		throw new JsonException($"无法将 {value} 转换为 double 类型。");
	}

	public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
	{
		writer.WriteNumberValue(value * 100);
	}
}

很好理解,只需要继承自JsonConverter类,并重写它的Read和Write方法即可。

上面的JSON里面还有一些字段比如head不需要就不用定义。现在以上代码能够正常运行,但是仍有瑕疵。

在ShiborResponse类里面有一个ShiborDateInfo类型的字段,这个类里面只有一个类型为DateTime的ShowDate字段 (这个类如果对照JSON应该有两个字段,showDateCN和showDateEN,因为不需要showDateEN),这里最好的办法是移除ShbiorDateInfo类,将该类的ShowDate字段放到ShiborResponse类里来。

// 定义完整响应类
class ShiborResponse
{
	[JsonPropertyName("showDateCN")]
	[JsonConverter(typeof(CustomDateTimeConverter))]
	public DateTime ShowDate { get; set; }
	[JsonPropertyName("records")]
	public List<ShiborRecord> Records { get; set; }
}

现在如果直接运行代码,是肯定会报错的,因为C#类的结构和JSON的结构不对应,JSON文件中的showDateCN从data类移到了上一级里面,所以需要再添加一个手动的解析:

//自定义 ShiborResponse 转换器
class ShiborResponseConverter : JsonConverter<ShiborResponse>
{
	public override ShiborResponse Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (reader.TokenType != JsonTokenType.StartObject)
		{
			throw new JsonException();
		}

		var response = new ShiborResponse();
		while (reader.Read())
		{
			if (reader.TokenType == JsonTokenType.PropertyName)
			{
				string propertyName = reader.GetString();
				reader.Read();
				switch (propertyName)
				{
					case "records":
						response.Records = JsonSerializer.Deserialize<List<ShiborRecord>>(ref reader, options);
						break;
					case "data":
						if (reader.TokenType == JsonTokenType.StartObject)
						{
							while (reader.Read())
							{
								if (reader.TokenType == JsonTokenType.EndObject)
								{
									break;
								}

								if (reader.TokenType == JsonTokenType.PropertyName)
								{
									string dataPropertyName = reader.GetString();
									reader.Read();
									if (dataPropertyName == "showDateCN")
									{
										response.ShowDate = new CustomDateTimeConverter().Read(ref reader, typeof(DateTime), options);
									}
								}
							}
						}
						break;
				}
			}
		}
		return response;
	}

	public override void Write(Utf8JsonWriter writer, ShiborResponse value, JsonSerializerOptions options)
	{
		writer.WriteStartObject();

		writer.WritePropertyName("Records");
		JsonSerializer.Serialize(writer, value.Records, options);

		writer.WritePropertyName("Data");
		writer.WriteStartObject();
		new CustomDateTimeConverter().Write(writer, value.ShowDate, options);
		writer.WriteEndObject();

		writer.WriteEndObject();
	}
}

是这个ShiborResponseConverter基本上就是对字符按照JSON的结构进行手动解析了。

现在运行测试代码:

void Main()
{
	var shiborData = GetShiborData();
	if (shiborData != null)
	{
		Console.WriteLine($"显示日期(中文): {shiborData.ShowDate}");
		Console.WriteLine("Shibor 记录信息:");
		foreach (var record in shiborData.Records)
		{
			Console.WriteLine($"期限代码: {record.TermCode}, Shibor 值: {record.Shibor}");
		}
	}
}

可以看到输出结果如下:

显示日期(中文): 2025/4/18 11:00:00
Shibor 记录信息:
期限代码: Overnight, Shibor 值: 0.0166
期限代码: OneWeek, Shibor 值: 0.01654
期限代码: TwoWeeks, Shibor 值: 0.01764
期限代码: OneMonth, Shibor 值: 0.01763
期限代码: ThreeMonths, Shibor 值: 0.01761
期限代码: SixMonths, Shibor 值: 0.01771
期限代码: NineMonths, Shibor 值: 0.01775
期限代码: OneYear, Shibor 值: 0.01772

相当完美。

这里也贴出用Newtonsoft.Json 库解析的类定义和转换代码,它与 System.Text.Json 有些不同,但大同小异:

数据获取,这里因为是采用Https请求,所以要首先定义SecurityProtocol,否则在一些操作系统上会报错:

/// <summary>
/// 获取最新的 Shibor 数据,如果未获取到数据,返回null
/// </summary>
/// <returns></returns>
public static ShiborResult GetShibors()
{
    System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
    var url = "https://www.shibor.org/r/cms/www/chinamoney/data/shibor/shibor.json";
    var httpClient = new HttpClient();
    try
    {
        // 创建一个空的请求内容(如果 POST 请求需要传递参数,这里可以设置)
        var content = new StringContent("{}", Encoding.UTF8, "application/json");
        var response = httpClient.PostAsync(url, content).Result;
        if (response.IsSuccessStatusCode)
        {
            var responseContent = response.Content.ReadAsStringAsync().Result;
            return JsonConvert.DeserializeObject<ShiborResult>(responseContent);

        }
    }
    catch (Exception ex)
    {
        //Console.WriteLine($"发生错误: {ex.Message}");
    }
    return null;
}

实体定义,以及对应的转换方法:

[JsonConverter(typeof(ShiborResultConverter))]
public class ShiborResult
{
    public DateTime ShowDate { get; set; }
    public List<ShiborRecord> Records { get; set; }
}

public enum TermCode
{
    [JsonProperty("O/N")]
    Overnight,
    [JsonProperty("1W")]
    OneWeek,
    [JsonProperty("2W")]
    TwoWeeks,
    [JsonProperty("1M")]
    OneMonth,
    [JsonProperty("3M")]
    ThreeMonths,
    [JsonProperty("6M")]
    SixMonths,
    [JsonProperty("9M")]
    NineMonths,
    [JsonProperty("1Y")]
    OneYear
}
// 定义记录信息类
public class ShiborRecord
{
    [JsonProperty("termCode")]
    [JsonConverter(typeof(CustomTermCodeConverter))]
    public TermCode TermCode { get; set; }

    [JsonProperty("shibor")]
    [JsonConverter(typeof(CustomShiborConverter))]
    public double Shibor { get; set; }
}

// 自定义日期时间转换器
public class CustomDateTimeConverter : IsoDateTimeConverter
{
    public CustomDateTimeConverter()
    {
        DateTimeFormat = "yyyy-MM-dd HH:mm";
    }
}

// 自定义枚举转换器
public class CustomTermCodeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TermCode);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string value = reader.Value.ToString();
        switch (value)
        {
            case "O/N":
                return TermCode.Overnight;
            case "1W":
                return TermCode.OneWeek;
            case "2W":
                return TermCode.TwoWeeks;
            case "1M":
                return TermCode.OneMonth;
            case "3M":
                return TermCode.ThreeMonths;
            case "6M":
                return TermCode.SixMonths;
            case "9M":
                return TermCode.NineMonths;
            case "1Y":
                return TermCode.OneYear;
            default:
                throw new JsonException($"无法将 {value} 转换为 TermCode 枚举类型。");
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        string termCodeString;
        TermCode termCode = (TermCode)value;
        switch (termCode)
        {
            case TermCode.Overnight:
                termCodeString = "O/N";
                break;
            case TermCode.OneWeek:
                termCodeString = "1W";
                break;
            case TermCode.TwoWeeks:
                termCodeString = "2W";
                break;
            case TermCode.OneMonth:
                termCodeString = "1M";
                break;
            case TermCode.ThreeMonths:
                termCodeString = "3M";
                break;
            case TermCode.SixMonths:
                termCodeString = "6M";
                break;
            case TermCode.NineMonths:
                termCodeString = "9M";
                break;
            case TermCode.OneYear:
                termCodeString = "1Y";
                break;
            default:
                throw new JsonException($"无法将 {termCode} 转换为字符串。");
        }
        writer.WriteValue(termCodeString);
    }
}

// 自定义 Shibor 字段转换器
public class CustomShiborConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(double);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string value = reader.Value.ToString();
        if (double.TryParse(value, out double shiborValue))
        {
            return shiborValue / 100;
        }
        throw new JsonException($"无法将 {value} 转换为 double 类型。");
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue((double)value * 100);
    }
}

// 自定义 ShiborResponse 转换器
public class ShiborResultConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ShiborResult);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var response = new ShiborResult();
        JObject jObject = JObject.Load(reader);

        if (jObject["records"] != null)
        {
            response.Records = jObject["records"].ToObject<List<ShiborRecord>>();
        }

        if (jObject["data"] != null && jObject["data"]["showDateCN"] != null)
        {
            response.ShowDate = jObject["data"]["showDateCN"].ToObject<DateTime>();
        }

        return response;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var shiborResponse = (ShiborResult)value;
        JObject jObject = new JObject();

        JArray recordsArray = JArray.FromObject(shiborResponse.Records);
        jObject["records"] = recordsArray;

        JObject dataObject = new JObject();
        dataObject["showDateCN"] = shiborResponse.ShowDate;
        jObject["data"] = dataObject;

        jObject.WriteTo(writer);
    }
}