如何修复反序列化 JSON 时的 InvalidOperationException?
结论
这个异常是因为 BankAccount 的构造函数参数无法全部绑定到可反序列化成员。
问题有两处:
- 构造函数参数
initialAmount没有同名属性或字段。对应的类成员是Amount,因此参数应改为amount。 AccountId、DateCreated和PIN使用了字段。如果保留这种设计,需要在反序列化所用的JsonSerializerOptions中启用IncludeFields,并保证序列化和反序列化使用相同的配置。
建议把这些字段改成只读属性,同时让构造函数参数名与属性名一致。
推荐修改:使用属性进行构造函数绑定
using System;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
namespace ReadOnlyBankingSystem.Models
{
public class BankAccount
{
public Guid AccountId { get; }
public DateTime DateCreated { get; }
public int PIN { get; }
public decimal Amount { get; set; }
public ObservableCollection<Transaction> Transactions { get; set; }
[JsonConstructor]
public BankAccount(
Guid accountId,
DateTime dateCreated,
int pin,
decimal amount)
{
AccountId = accountId;
DateCreated = dateCreated;
PIN = pin;
Amount = amount;
Transactions = new ObservableCollection<Transaction>();
}
}
}
需要将:
decimal initialAmount
改为:
decimal amount
System.Text.Json 会根据名称和类型,把构造函数参数绑定到类成员。名称匹配不区分大小写:
accountId对应AccountIddateCreated对应DateCreatedpin对应PINamount对应Amount
这里匹配的是 C# 成员名称,与构造函数内部把参数赋给哪个成员无关,也不使用经过 JSON 命名策略转换后的名称。
initialAmount 找不到对应的 InitialAmount 属性或字段,所以反序列化器无法使用这个构造函数。
如果类中只有一个公开构造函数,通常可以省略 [JsonConstructor]。保留该特性可以明确指定 JSON 反序列化所用的构造函数,也能避免以后增加其他构造函数时产生选择歧义。
如果必须保留字段
如果不想把 AccountId、DateCreated 和 PIN 改成属性,可以继续使用字段,但仍需把 initialAmount 改为 amount:
public class BankAccount
{
[JsonInclude]
public readonly Guid AccountId;
[JsonInclude]
public readonly DateTime DateCreated;
[JsonInclude]
public readonly int PIN;
public decimal Amount { get; set; }
public ObservableCollection<Transaction> Transactions { get; set; }
[JsonConstructor]
public BankAccount(
Guid accountId,
DateTime dateCreated,
int pin,
decimal amount)
{
AccountId = accountId;
DateCreated = dateCreated;
PIN = pin;
Amount = amount;
Transactions = new ObservableCollection<Transaction>();
}
}
还要为源生成上下文启用字段处理:
using System.Collections.Generic;
using System.Text.Json.Serialization;
[JsonSourceGenerationOptions(
IncludeFields = true,
WriteIndented = true)]
[JsonSerializable(typeof(Dictionary<string, Customer>))]
internal sealed partial class CustomerContext : JsonSerializerContext
{
}
[JsonInclude] 可以显式包含字段,启用 IncludeFields 则能清楚表明这些字段需要参与源生成配置和构造函数绑定。考虑到后续维护,属性通常比公开只读字段更省事。
统一序列化和反序列化配置
目前读取时使用 CustomerContext,写入时则新建了另一套 JsonSerializerOptions:
JsonSerializer.Deserialize(
contents,
CustomerContext.Default.DictionaryStringCustomer);
JsonSerializer.Serialize(customerData, options);
这会导致读写配置不一致。例如,字段包含规则、命名规则或自定义转换器可能只在一条路径中生效。建议读写都使用生成的上下文:
public async Task<Dictionary<string, Customer>> ReadFromJsonFile(
string filename)
{
string appDataFilePath = GetAppDataFilePath(filename);
string contents = await File.ReadAllTextAsync(appDataFilePath);
return JsonSerializer.Deserialize(
contents,
CustomerContext.Default.DictionaryStringCustomer)
?? new Dictionary<string, Customer>();
}
public async Task WriteToJsonFile(
string filename,
Dictionary<string, Customer> customerData)
{
string appDataFilePath = GetAppDataFilePath(filename);
string contents = JsonSerializer.Serialize(
customerData,
CustomerContext.Default.DictionaryStringCustomer);
await File.WriteAllTextAsync(appDataFilePath, contents);
}
WriteIndented = true 已经配置在 [JsonSourceGenerationOptions] 中,写入时无需再创建单独的 JsonSerializerOptions。
这里改用 File.WriteAllTextAsync 也能避免旧内容残留,因为它会覆盖原文件。原代码中的 File.OpenWrite 不一定会截断旧内容。如果新 JSON 比原文件短,文件末尾可能留下旧字符,之后读取时就会触发 JsonException。这个问题与当前的构造函数异常无关,但可以顺手修正。
也可以使用无参构造函数
另一种方案是把所有需要恢复的成员改为可写属性,并提供无参构造函数:
public class BankAccount
{
public Guid AccountId { get; set; }
public DateTime DateCreated { get; set; }
public int PIN { get; set; }
public decimal Amount { get; set; }
public ObservableCollection<Transaction> Transactions { get; set; }
= new ObservableCollection<Transaction>();
public BankAccount()
{
}
public BankAccount(
Guid accountId,
DateTime dateCreated,
int pin,
decimal initialAmount)
{
AccountId = accountId;
DateCreated = dateCreated;
PIN = pin;
Amount = initialAmount;
}
}
这种写法不依赖参数绑定。反序列化器会先调用无参构造函数,再依次设置各个属性。
代价是账户标识、创建时间和 PIN 会从只读成员变成可修改属性。如果这些值在业务中应保持不变,更适合使用前面的构造函数绑定方案。
还需要检查 Transaction
CustomerContext 会顺着对象关系处理 Customer、BankAccount 和 Transaction。如果 Transaction 也只有带参数的构造函数,需要检查以下内容:
- 每个构造函数参数是否都有名称和类型匹配的属性或已包含字段。
- 不要使用
initialAmount、transactionValue这类业务别名去绑定名为Amount的成员。 - 如果存在多个构造函数,应使用
[JsonConstructor]指定反序列化构造函数。 - 如果使用字段,应确保上下文启用了相应的字段配置。
当前异常可以直接通过把 initialAmount 改成 amount 来修复。更适合长期维护的方案是使用只读属性,并统一通过 CustomerContext 完成序列化和反序列化。
真实的银行类应用不应把明文密码或 PIN 直接写入 JSON 文件。密码通常应保存为带盐哈希,其他敏感数据也需要使用平台安全存储或合适的加密方案。
备注:内容仅供参考。
