You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 多态DTO实现:学生测试REST API答题提交的反序列化问题解决方案

解决ASP.NET Core中多态答题DTO的反序列化问题

这个问题在处理多态DTO的JSON反序列化时很常见——System.Text.Json默认无法自动根据type字段区分子类,导致子类的专属属性(比如givenAnswer、selectedAnswerOptionId)丢失。不过我们可以通过自定义Json转换器完美解决这个问题,下面是具体的实现步骤:

1. 定义提交测试的顶层DTO

首先创建一个对应你JSON结构的提交请求类:

public class SubmitTestRequest
{
    public Guid TestId { get; set; }
    public ICollection<AnsweredQuestion> AnsweredQuestions { get; set; }
}

2. 实现自定义Json转换器

核心逻辑是编写一个JsonConverter<AnsweredQuestion>,根据type字段的值选择对应的子类进行反序列化:

public class AnsweredQuestionConverter : JsonConverter<AnsweredQuestion>
{
    public override AnsweredQuestion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException("Invalid JSON format: expected object start.");
        }

        // 先把整个JSON对象解析为JsonElement,方便读取type字段
        using var doc = JsonDocument.ParseValue(ref reader);
        var rootElement = doc.RootElement;

        // 读取并校验type字段
        if (!rootElement.TryGetProperty("type", out var typeProperty) || string.IsNullOrEmpty(typeProperty.GetString()))
        {
            throw new JsonException("Missing or invalid 'type' property in question.");
        }

        // 根据type值匹配对应的子类并反序列化
        var questionType = typeProperty.GetString();
        AnsweredQuestion question = questionType switch
        {
            "text" => JsonSerializer.Deserialize<AnsweredTextQuestion>(rootElement.GetRawText(), options),
            "single-choice" => JsonSerializer.Deserialize<AnsweredSingleChoiceQuestion>(rootElement.GetRawText(), options),
            "multiple-choice" => JsonSerializer.Deserialize<AnsweredMultipleChoiceQuestion>(rootElement.GetRawText(), options),
            _ => throw new JsonException($"Unsupported question type: {questionType}")
        };

        // 兜底检查并赋值Id(子类反序列化时会自动处理,这里做额外校验)
        if (rootElement.TryGetProperty("id", out var idProperty) && idProperty.TryGetGuid(out var id))
        {
            question.Id = id;
        }

        return question;
    }

    public override void Write(Utf8JsonWriter writer, AnsweredQuestion value, JsonSerializerOptions options)
    {
        // 序列化时使用子类的实际类型,确保所有属性(包括Type)都被正确输出
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

3. 配置转换器生效

有两种方式让System.Text.Json使用这个转换器:

方式一:全局配置(推荐)

在Startup.cs的ConfigureServices方法中添加全局JSON配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddJsonOptions(options =>
        {
            // 添加自定义转换器
            options.JsonSerializerOptions.Converters.Add(new AnsweredQuestionConverter());
            // 可选:启用驼峰命名匹配你的JSON字段(比如testId、answeredQuestions)
            options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        });
}

方式二:通过特性标记基类

直接在AnsweredQuestion抽象类上添加特性,指定转换器:

[JsonConverter(typeof(AnsweredQuestionConverter))]
public abstract class AnsweredQuestion
{
    public Guid Id { get; set; }
    public abstract string Type { get; }
}

4. 在API接口中使用

现在你可以在Controller的Action中直接接收SubmitTestRequest参数,反序列化会自动处理不同题型的子类实例:

[HttpPost("submit-test")]
public IActionResult SubmitTest([FromBody] SubmitTestRequest request)
{
    foreach (var answeredQuestion in request.AnsweredQuestions)
    {
        switch (answeredQuestion)
        {
            case AnsweredTextQuestion textQuestion:
                // 处理文本题:使用textQuestion.GivenAnswer
                Console.WriteLine($"Text question answer: {textQuestion.GivenAnswer}");
                break;
            case AnsweredSingleChoiceQuestion singleChoiceQuestion:
                // 处理单选题:使用singleChoiceQuestion.SelectedAnswerOptionId
                Console.WriteLine($"Single choice selected option: {singleChoiceQuestion.SelectedAnswerOptionId}");
                break;
            case AnsweredMultipleChoiceQuestion multipleChoiceQuestion:
                // 处理多选题:使用multipleChoiceQuestion.SelectedAnswerOptionIds
                Console.WriteLine($"Multiple choice selected options: {string.Join(", ", multipleChoiceQuestion.SelectedAnswerOptionIds)}");
                break;
        }
    }

    return Ok("Test submitted successfully");
}

为什么默认方式不行?

System.Text.Json在处理抽象类/接口的反序列化时,默认只会尝试创建基类的实例(甚至直接抛出异常),不会自动识别子类的专属属性。通过自定义转换器,我们可以根据type字段明确指定要实例化的子类,从而完整保留所有题型的特定数据。

内容的提问来源于stack exchange,提问作者satma0745

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 06:04:05