JSON序列化时如何将父对象Type和ID注入子Service的URI?
解决JSON序列化时子对象获取父对象属性生成URI的问题
这个问题我之前也碰到过——JSON.NET的IValueProvider确实是在模型契约层面工作的,没法直接拿到父对象的运行时值。不过有两个比较优雅的解决思路,我给你详细拆解:
方案一:自定义JsonConverter处理父对象(推荐)
这个方法直接在序列化Person对象时,主动处理子集合Services,把父对象的Type和ID注入到ServiceUri的占位符中,逻辑清晰且不依赖上下文。
步骤1:实现Person的自定义转换器
public class PersonConverter : JsonConverter<Person> { public override void WriteJson(JsonWriter writer, Person person, JsonSerializer serializer) { // 先把Person序列化为JObject,保留原有序列化逻辑 var personObj = JObject.FromObject(person, serializer); // 处理Services集合 if (person.Services != null) { var servicesArray = new JArray(); foreach (var service in person.Services) { // 序列化单个Service var serviceObj = JObject.FromObject(service, serializer); // 替换占位符生成真实URI var resolvedUri = service.ServiceUri .Replace("{type}", person.Type) .Replace("{id}", person.ID.ToString()); // 更新ServiceUri字段 serviceObj["serviceUri"] = resolvedUri; servicesArray.Add(serviceObj); } // 替换原有的services节点 personObj["services"] = servicesArray; } // 最终写入JSON personObj.WriteTo(writer); } public override Person ReadJson(JsonReader reader, Type objectType, Person existingValue, bool hasExistingValue, JsonSerializer serializer) { // 如果不需要自定义反序列化,直接用默认逻辑即可 return serializer.Deserialize<Person>(reader); } }
步骤2:注册转换器并序列化
在序列化Person时,把自定义转换器添加到JsonSerializerSettings中:
var person = new Person { ID = Guid.Parse("ae330dc8-0217-4c58-b64b-44a1c7ba4f46"), Type = "Person", Name = "Fred", Services = new List<Service> { new Service { ServiceName = "ListNotes", ServiceUri = "https://server/api/notes/{type}/{id}" }, new Service { ServiceName = "ListContactDetails", ServiceUri = "https://server/api/contacts/{type}/{id}" } } }; var settings = new JsonSerializerSettings(); settings.Converters.Add(new PersonConverter()); // 格式化输出方便查看 settings.Formatting = Formatting.Indented; var json = JsonConvert.SerializeObject(person, settings);
这个方案的优势是:
- 完全保留你原有的
IValueProvider逻辑(比如ObjectUri的处理) - 不依赖全局上下文,避免嵌套序列化时的冲突
- 代码逻辑直观,后期维护成本低
方案二:利用JsonSerializer上下文传递父对象信息
如果你不想修改序列化流程,也可以通过JSON.NET的StreamingContext传递父对象的运行时值,让ServiceUriValueProvider能读取到这些信息。
步骤1:修改ServiceUriValueProvider
让它从序列化上下文中获取父对象的Type和ID:
public class ServiceUriValueProvider : IValueProvider { private readonly PropertyInfo _targetProperty; public ServiceUriValueProvider(PropertyInfo targetProperty) { _targetProperty = targetProperty; } public void SetValue(object target, object value) { _targetProperty.SetValue(target, value); } public object GetValue(object target) { var service = target as Service; if (service == null) return null; // 从当前序列化上下文获取父对象信息 var currentSerializer = JsonSerializer.Current; if (currentSerializer?.Context.Items != null) { if (currentSerializer.Context.Items.TryGetValue("ParentType", out var typeObj) && currentSerializer.Context.Items.TryGetValue("ParentId", out var idObj)) { var type = typeObj as string; var id = idObj as Guid?; if (!string.IsNullOrEmpty(type) && id.HasValue) { var template = _targetProperty.GetValue(target) as string; return template?.Replace("{type}", type).Replace("{id}", id.Value.ToString()); } } } // 没有上下文时返回原模板 return _targetProperty.GetValue(target); } }
步骤2:序列化时注入上下文
在序列化Person前,把父对象的Type和ID存入StreamingContext:
var settings = new JsonSerializerSettings { ContractResolver = new CustomContractResolver(), // 你的自定义契约解析器,已注册ServiceUriValueProvider Formatting = Formatting.Indented }; var serializer = JsonSerializer.Create(settings); // 注入父对象信息到上下文 serializer.Context = new StreamingContext(StreamingContextStates.Other, new Dictionary<string, object> { ["ParentType"] = person.Type, ["ParentId"] = person.ID }); using (var sw = new StringWriter()) using (var writer = new JsonTextWriter(sw)) { serializer.Serialize(writer, person); var json = sw.ToString(); }
这个方案适合需要保留IValueProvider统一逻辑的场景,但要注意JsonSerializer.Current仅在Json.NET 6.0+和.NET Framework 4.5+支持,且多层嵌套序列化时上下文可能被覆盖。
内容的提问来源于stack exchange,提问作者David Waldock
相关产品推荐
相关产品推荐

