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
相关产品推荐
相关产品推荐

