如何在不调用构造函数的情况下反序列化类?

20
我在我的WCF数据服务中使用Json.NET。
这是我的类(简化版):
[DataContract]
public class Component
{
    public Component()
    {
        // I'm doing some magic here.
    }
}

我如何在使用JsonConvert.DeserializeObject时,不调用构造函数进行反序列化?

如果不清楚,请随时问问题。


据我所知,这是不可能的。当创建一个实例时,构造函数总是会被执行。 - Maarten
3
System.Runtime.Serialization.FormatterServices.GetSafeUninitializedObject 方法返回一个指定类型的新实例,该实例还没有初始化。请注意,该方法不会调用任何构造函数,因此返回的对象可能处于不稳定状态,并且需要进一步初始化才能安全地使用。建议仅在特定情况下使用此方法,例如在非常规序列化场景中需要创建未初始化对象的情况下。 - Igor Shastin
谢谢!我今天学到了东西 :-) - Maarten
您可以添加一个带有逻辑的“Initialize”方法。不幸的是,这需要更改消费代码。 - Steve B
@SteveB:如果有人忘记调用初始化方法(或者不知道必须调用它们),那么初始化方法将会失败。 - jgauffin
1
@jgauffin:你是对的。你的解决方案更好,但如果没有参数传递给构造函数怎么办? - Steve B
4个回答

12

构造函数总是被调用。我通常有两个构造函数,一个是序列化的(默认构造函数),另一个是为所有“普通”代码而设计的:

构造函数总是被调用。我通常有两个构造函数,一个是 序列化 的(默认构造函数),另一个是为所有“普通”代码而设计的:

[DataContract]
public class Component
{
    // for JSON.NET
    protected Component()
    {
    }

    public Component(allMandatoryFieldsHere)
    {
        // I'm doing some magic here.
    }
}

这样我也可以确保开发人员指定所有必需的信息。

但是,我并不真正推荐在传输信息时使用除DTO之外的任何东西,因为否则可能会绕过对象的封装(任何人都可以使用任何值初始化任何字段)。如果您使用的不是贫血模型,那么就会出现这种情况。

因此,在我看来,使用FormatterServices.GetSafeUninitializedObject是一种丑陋的解决方法,不会有人知道您以未初始化的方式创建了所有对象。构造函数初始化是有原因的。最好通过提供“序列化”构造函数让类知道无需调用真正的构造函数就可以正常工作,就像我建议的那样。


但据我所知,DataContractJsonSerializer 默认情况下不会调用构造函数。 - Igor Shastin
1
@IgorShastin:这就是为什么你只应该在DTO中使用它。JSON.NET更加表现良好。 - jgauffin

8
  1. 你可以创建一个继承自CustomCreationConverter的类,并使用FormatterServices.GetSafeUninitializedObject创建对象,这样可以跳过调用构造函数。

    关于CustomCreationConverter的更多信息请点击这里

  2. 在一个类上添加[JsonObject(MemberSerialization.Fields)]将使Json.NET默认使用FormatterServices.GetSafeUninitializedObject(虽然Fields模式也会序列化公共/私有字段而不是公共属性,这可能不是你想要的)。

  3. 将不想运行的逻辑移出默认构造函数。


非常感谢!由于某种原因,当我尝试使用[JsonObject(MemberSerialization.Fields)]时,会出现StackOverflowException异常。所以我更喜欢第一种方式。请问是否有办法为多个类创建通用的CustomCreationConverter - Igor Shastin
1
复制CustomCreationConverter源代码并修改它以实现你想要的功能。它只是继承自JsonConverter - https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs - James Newton-King

3

其他人已经提到了第二个构造函数,但是使用2个属性:[JsonConstructor]和[Obsolete],您可以比让人们记住要调用哪一个更好地完成任务。

    public ChatMessage()
    {   
        MessageID = ApplicationState.GetNextChatMessageID(); // An expensive call that uses up an otherwise free ID from a limited set and does disk access in the process.
    }


    [JsonConstructor] // This forces JsonSerializer to call it instead of the default.
    [Obsolete("Call the default constructor. This is only for JSONserializer", true)] // To make sure that calling this from your code directly will generate a compiler error. JSONserializer can still call it because it does it via reflection.
    public ChatMessage(bool DO_NOT_CALL_THIS)
    {
    }

[JsonConstructor] 强制 JsonSerializer 调用它而不是默认构造函数。
[Obsolete("...", true)] 确保直接从代码中调用此方法将生成编译器错误。但 JSONserializer 仍然可以通过反射来调用它。


1
避免反序列化时构造函数的调用的最佳选项是创建一个特殊的合同解析器,覆盖所有没有标记JsonConstructor属性的构造函数的类的创建函数。这样,如果你真的需要它,你仍然可以强制JSON.NET调用构造函数,但所有其他类将像在.NET标准DataContract序列化程序中一样被创建。以下是代码:
/// <summary>
/// Special contract resolver to create objects bypassing constructor call.
/// </summary>
public class NoConstructorCreationContractResolver : DefaultContractResolver
{
    /// <summary>
    /// Creates a <see cref="T:Newtonsoft.Json.Serialization.JsonObjectContract"/> for the given type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    /// A <see cref="T:Newtonsoft.Json.Serialization.JsonObjectContract"/> for the given type.
    /// </returns>
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        // prepare contract using default resolver
        var objectContract = base.CreateObjectContract(objectType);

        // if type has constructor marked with JsonConstructor attribute or can't be instantiated, return default contract
        if (objectContract.OverrideCreator != null || objectContract.CreatedType.IsInterface || objectContract.CreatedType.IsAbstract)
            return objectContract;

        // prepare function to check that specified constructor parameter corresponds to non writable property on a type
        Func<JsonProperty, bool> isParameterForNonWritableProperty =
            parameter =>
            {
                var propertyForParameter =
                    objectContract.Properties.FirstOrDefault(property => property.PropertyName == parameter.PropertyName);

                if (propertyForParameter == null)
                    return false;

                return !propertyForParameter.Writable;
            };

        // if type has parameterized constructor and any of constructor parameters corresponds to non writable property, return default contract
        // this is needed to handle special cases for types that can be initialized only via constructor, i.e. Tuple<>
        if (objectContract.CreatorParameters.Any(parameter => isParameterForNonWritableProperty(parameter)))
            return objectContract;

        // override default creation method to create object without constructor call
        objectContract.DefaultCreatorNonPublic = false;
        objectContract.DefaultCreator = () => FormatterServices.GetSafeUninitializedObject(objectContract.CreatedType);

        return objectContract;
    }
}

您只需要在反序列化之前将此合同解析器设置在序列化程序设置中即可。

关于只读字段和只读属性的更新

如果一个只读字段有一个JsonProperty属性,则它可以被反序列化。 如果一个只读属性的后台字段有一个JsonProperty属性,则它也可以被反序列化。 自C# 7.3以来,可以将属性应用于自动属性的编译器生成的后台字段。

利用这一点,我们可以这样做:

public class SampleClass
{
    [JsonProperty("Field1")]
    private readonly string _field1 = "Field1Value";

    [JsonProperty("Property1")]
    private string _property1BackingField = "Property1Value";

    [JsonIgnore]
    public string Property1 => _property1BackingField;

    [field: JsonProperty("Property2")]
    [JsonIgnore]
    public string Property2 { get; } = "Property2Value";
}

const string json = "{\"Field1\":\"NEW-Field1Value\",\"Property1\":\"NEW-Property1Value\",\"Property2\":\"NEW-Property2Value\"}";

var serializerSettings = new JsonSerializerSettings
{
    ContractResolver = new DefaultContractResolver
    {
        // this settings is needed to work with compiler-generated backing fields
        SerializeCompilerGeneratedMembers = true
    }
};

var deserializedSample = JsonConvert.DeserializeObject<SampleClass>(json, serializerSettings);

如果您需要在旧版C#中执行此操作或对属性没有控制权,则可以使用类似以下内容的方式覆盖DefaultContractResolver的CreateProperty()方法:
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
    var jsonProperty = base.CreateProperty(member, memberSerialization);

    if (jsonProperty.Writable == false)
    {
        // this uses compiler implementation details and may not work for all cases
        // better to use smarter approach like in BackingFieldResolver from Mono.Reflection library
        var fieldInfo = jsonProperty.DeclaringType.GetField($"<{jsonProperty.PropertyName}>k__BackingField",
            BindingFlags.NonPublic | BindingFlags.Instance);

        if (fieldInfo != null)
        {
            jsonProperty.ValueProvider = new ReflectionValueProvider(fieldInfo);
            jsonProperty.Writable = true;
        }
    }
        
    return jsonProperty;
}

objectContract.OverrideConstructor 属性不存在。 - flayn
@flayn 是的,它已经被更改为 OverrideCreator。我已经更新了代码片段。 - Alexander
@Alexander 你知道如何扩展这个过程,以使反序列化过程也能写入只读属性吗? - Erutan409
1
@Erutan409,我已经添加了关于只读字段和只读属性的想法。希望这能帮到你。 - Alexander

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