JSON.NET错误:检测到类型的自引用循环。

620

我尝试序列化从Entity Data Model .edmx自动生成的POCO类,当我使用以下代码:

JsonConvert.SerializeObject 

我遇到了以下错误:

发生类型为System.data.entity的自引用循环检测错误。

我该怎么解决这个问题?


当您使用Linq和MVC时:http://stackoverflow.com/a/38241856 - aDDin
当使用.NET Core 2时:https://dev59.com/EWs05IYBdhLWcg3wLO-Q#48709134 - Dave Skender
7
当我想要序列化一个异步方法调用的结果(一个Task)时,由于忘记在前面加上await语句,导致出现了这个错误。 - Uwe Keim
28个回答

590

最佳解决方案来自Web API中的循环引用处理(本回答大部分内容均来自此处):

修复方法1:全局忽略循环引用

(我选择/尝试了这个解决方案,许多其他人也是如此)

Json.net序列化器有一个选项可以忽略循环引用。将以下代码放入WebApiConfig.cs文件中:

 config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling 
= Newtonsoft.Json.ReferenceLoopHandling.Ignore; 

简单的修复方法会使序列化程序忽略引用,从而导致循环。然而,它有以下限制:

  • 数据失去了循环引用的信息
  • 该修复方法仅适用于JSON.net
  • 如果存在深层的引用链,则无法控制引用的级别

如果您想在非API ASP.NET项目中使用此修复程序,请将上述行添加到Global.asax.cs,但首先添加:

var config = GlobalConfiguration.Configuration;

如果您想在 .Net Core 项目中使用此功能,您可以将 Startup.cs 更改为:

var mvc = services.AddMvc(options =>
{
   ...
})
.AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);

修复方法2:全局保留循环引用

第二种修复方法与第一种类似。只需将代码更改为:

config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling 
     = Newtonsoft.Json.ReferenceLoopHandling.Serialize;     
config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling 
     = Newtonsoft.Json.PreserveReferencesHandling.Objects;

应用此设置后,数据形状将发生变化。

[
   {
      "$id":"1",
      "Category":{
         "$id":"2",
         "Products":[
            {
               "$id":"3",
               "Category":{
                  "$ref":"2"
               },
               "Id":2,
               "Name":"Yogurt"
            },
            {
               "$ref":"1"
            }
         ],
         "Id":1,
         "Name":"Diary"
      },
      "Id":1,
      "Name":"Whole Milk"
   },
   {
      "$ref":"3"
   }
]

$id和$ref保持了所有引用并使对象图的层级变得扁平,但客户端代码需要知道形状的变化以消费数据,它只适用于JSON.NET序列化器。

修复3:忽略和保留引用属性

此修复装饰模型类上的属性以控制模型或属性级别的序列化行为。要忽略属性:

public class Category 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
   
    [JsonIgnore] 
    [IgnoreDataMember] 
    public virtual ICollection<Product> Products { get; set; } 
} 

JsonIgnore是用于JSON.NET的,而IgnoreDataMember是用于XmlDCSerializer的。 为保留引用:

// Fix 3 
[JsonObject(IsReference = true)] 
public class Category 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
  
    // Fix 3 
    //[JsonIgnore] 
    //[IgnoreDataMember] 
    public virtual ICollection<Product> Products { get; set; } 
} 
 
[DataContract(IsReference = true)] 
public class Product 
{ 
    [Key] 
    public int Id { get; set; } 
 
    [DataMember] 
    public string Name { get; set; } 
 
    [DataMember] 
    public virtual Category Category { get; set; } 
}

JsonObject(IsReference = true)] 是用于 JSON.NET 的,而 [DataContract(IsReference = true)] 则是用于 XmlDCSerializer。请注意:在将 DataContract 应用于类之后,您需要将 DataMember 添加到要序列化的属性中。

这些属性既可以应用于 json 序列化器,也可以应用于 xml 序列化器,并且可以更好地控制模型类。


11
Fix 3对我有效。只需删除DataContract和DataMember属性,并在数据传输对象(DTO)上加上JsonObject(IsReference=true)即可。这样就可以了。谢谢。 - maestro
1
尝试这个: GlobalConfiguration.Configuration - Bishoy Hanna
1
修复3有一个优势,它可以在没有GlobalConfiguration的客户端代码上工作。 - dumbledad
1
@BishoyHanna,你能否编辑你的答案,使其可以从普通的ASP.NET应用程序中使用? 你可以使用我的建议进行编辑:https://stackoverflow.com/review/suggested-edits/17797683 - NH.
2
在属性上方使用[JsonIgnore]对我很有效。 - Nathan Beck
显示剩余2条评论

558

使用JsonSerializerSettings

  • ReferenceLoopHandling.Error(默认值)在遇到引用循环时会报错。这就是为什么你会收到异常。
  • ReferenceLoopHandling.Serialize 适用于嵌套但不是无限的对象。
  • ReferenceLoopHandling.Ignore 如果一个对象是它自己的子对象,则不会对其进行序列化。

例如:

JsonConvert.SerializeObject(YourPOCOHere, Formatting.Indented, 
new JsonSerializerSettings 
{ 
        ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});

如果您需要序列化一个嵌套无限的对象,您可以使用PreserveObjectReferences来避免StackOverflowException异常。

示例:

JsonConvert.SerializeObject(YourPOCOHere, Formatting.Indented, 
new JsonSerializerSettings 
{ 
        PreserveReferencesHandling = PreserveReferencesHandling.Objects
});

选择对你正在序列化的对象有意义的选项。

参考资料:http://james.newtonking.com/json/help/


75
在序列化datatable时,我遇到了错误。我使用了ReferenceLoopHandling = ReferenceLoopHandling.Ignore使其正常工作。 - user1873471
13
如果数据中存在引用循环,使用 ReferenceLoopHandling.Serialize 将导致序列化程序进入无限递归循环并导致堆栈溢出。 - Brian Rogers
2
正确。由于问题涉及到EF模型,这也是一个合理的关注点。已经修改以提供所有可用选项。 - DalSoft
1
当我尝试对一个对象进行序列化时,遇到了相同的错误...然而,该对象除了枚举类型之外没有其他引用。 - Marin
2
对我来说,EF是这个问题的主要原因,因为自引用实体到处都是。 - Teoman shipahi
对于问题即“检测到类型自引用循环”的情况,第三个选项可解决该问题,即ReferenceLoopHandling.Ignore - viking

83

解决方法是忽略循环引用并且不对它们进行序列化。这种行为在JsonSerializerSettings中有具体规定。

使用重载的单一JsonConvert

JsonConvert.SerializeObject(YourObject, Formatting.Indented,
    new JsonSerializerSettings() {
        ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
    }
);

全局设置,代码位于 Global.asax.cs 中的 Application_Start()

JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
     Formatting = Newtonsoft.Json.Formatting.Indented,
     ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
};

参考资料: https://github.com/JamesNK/Newtonsoft.Json/issues/78


当你进行全局设置时,为什么要将格式设置为缩进? - Murphybro2
绝对是我们解决这个问题所需要的(在部署期间发现)!你太棒了...感谢你节省我们的时间!! - Ryan Eastabrook
我通过在类“Startup.cs”中添加“JsonConvert.DefaultSettings”=()=> new JsonSerializerSettings {....}解决了我的问题。 - Beldi Anouar

52

最简单的方法是从nuget安装Json.NET,并将[JsonIgnore]属性添加到类中的虚拟属性,例如:

    public string Name { get; set; }
    public string Description { get; set; }
    public Nullable<int> Project_ID { get; set; }

    [JsonIgnore]
    public virtual Project Project { get; set; }

虽然最近我创建的模型只包含我需要传递的属性,使它更轻便,不包含不必要的集合,并且在重新生成文件时也不会丢失我的更改。


4
使用Newton JSON的最佳答案 - Aizen

21
在.NET Core 1.0中,您可以在Startup.cs文件中设置此全局设置:

using System.Buffers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Newtonsoft.Json;

// beginning of Startup class

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.OutputFormatters.Clear();
            options.OutputFormatters.Add(new JsonOutputFormatter(new JsonSerializerSettings(){
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            }, ArrayPool<char>.Shared));
        });
    }

但是在这种情况下,如果我想知道这个属性被忽略了,我将不会收到任何异常。 - Mayer Spitz

19
如果您正在使用.NET Core 2.x,请更新Startup.cs中的ConfigureServices部分。

https://learn.microsoft.com/en-us/ef/core/querying/related-data/serialization

    public void ConfigureServices(IServiceCollection services)
    {
    ...

    services.AddMvc()
        .AddJsonOptions(options => 
          options.SerializerSettings.ReferenceLoopHandling = 
            Newtonsoft.Json.ReferenceLoopHandling.Ignore
        );

    ...
    }

如果您使用的是不带MVC的.NET Core 3.x - 5.0版本,则应为:

# startup.cs
services.AddControllers()
  .AddNewtonsoftJson(options =>
      options.SerializerSettings.ReferenceLoopHandling =
        Newtonsoft.Json.ReferenceLoopHandling.Ignore
   );

对于 .NET 6.0,唯一的区别是现在它放在了 Program.cs 中。

# program.cs
services.AddControllers()
   .AddNewtonsoftJson(options =>
      options.SerializerSettings.ReferenceLoopHandling = 
        Newtonsoft.Json.ReferenceLoopHandling.Ignore);

如果您正在使用Entity Framework和数据库优先设计模式,则几乎必须进行此引用循环处理。


4
如果我不使用services.AddMvc()会发生什么? - prisar
2
这是一种不好的做法吗? - Renan Coelho
1
乍一看,您可能认为这是一种不好的做法,因为它可能会覆盖避免旧的“无限循环”问题的“有意设计”。但是,如果您考虑类的用例,您可能需要它们相互引用。例如,您可能希望访问Trees>Fruits和Fruits>Trees两个类。 - Dave Skender
1
此外,如果您正在使用类似于Entity Framework的数据库优先设计模式,并且根据您在数据库中设置外键的方式,它将自动创建这些循环引用,因此如果您要反向工程化您的类,则几乎必须使用此设置。 - Dave Skender
亲爱的戴夫·斯肯德,如何在.NET Core 6.0中修复?谢谢。 - HOÀNG LONG
显示剩余2条评论

10

如果您在使用NEWTONSOFTJSON进行序列化时遇到了循环问题,在我的情况下,我不需要修改global.asax或apiconfig文件。我只需使用JsonSerializesSettings并忽略循环处理即可。

JsonSerializerSettings jss = new JsonSerializerSettings();
jss.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
var lst = db.shCards.Where(m => m.CardID == id).ToList();
string json = JsonConvert.SerializeObject(lst, jss);

2
如果有人来这里寻找一行代码,以便在监视窗口中进行文本搜索: new Newtonsoft.Json.JsonSerializerSettings() {ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore});``` - Graham

10

我们可以将这两行代码添加到DbContext类的构造函数中,以禁用自引用循环,例如:

public TestContext()
        : base("name=TestContext")
{
    this.Configuration.LazyLoadingEnabled = false;
    this.Configuration.ProxyCreationEnabled = false;
}

这是其中最简单的一个,而且运行得非常好。投了赞成票,非常感谢... - Murat Yıldız
1
就像我在另一个问题中写的那样:我不喜欢这种回答,因为你关闭了EF6默认启用的一个功能,这段代码可能会破坏程序的其他部分。你应该解释一下这个代码是做什么的,以及它可能带来的后果。 - El Mac
@ElMac 你说得没错,但如果我们不需要那个功能,为什么不能使用这个解决方案呢? - Sanjay Nishad
1
@SanjayNishad 如果你不需要这个功能,我并不介意。只是有些用户不知道他们在禁用什么。 - El Mac

8

您也可以对属性应用属性。 [JsonProperty( ReferenceLoopHandling = ... )]属性非常适合此用途。

例如:

/// <summary>
/// Represents the exception information of an event
/// </summary>
public class ExceptionInfo
{
    // ...code omitted for brevity...

    /// <summary>
    /// An inner (nested) error.
    /// </summary>
    [JsonProperty( ReferenceLoopHandling = ReferenceLoopHandling.Ignore, IsReference = true )]
    public ExceptionInfo Inner { get; set; }

    // ...code omitted for brevity...    
}

希望这能帮到您,Jaans。

那就是我需要的那个。我的根对象继承自某个模型,但它也有一个具有相同模型的属性。当两者具有相同的ids值时,我遇到了这个自引用循环问题。在属性上添加忽略修饰符解决了这个问题。谢谢! - Kevin Cloet

4
在MVC 6中,要忽略循环引用并避免全局序列化,请在startup.cs中使用以下代码:
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().Configure<MvcOptions>(options =>
        {
            options.OutputFormatters.RemoveTypesOf<JsonOutputFormatter>();
            var jsonOutputFormatter = new JsonOutputFormatter();
            jsonOutputFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
            options.OutputFormatters.Insert(0, jsonOutputFormatter);
        });
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接