The target

In the SpringBoot interface, we typically annotate objects to be deserialized with the @RequestBody class, but in cases where there are multiple subclasses, the normal deserialization is not sufficient, such as:

We have an Exam class that represents an Exam:

@Data
public class Exam {

    private String name;
    private List<Question> questions;
}
Copy the code

Here, Question is special. Question itself is an abstract class that provides some general method calls. The actual subclasses include multiple choice questions, multiple choice questions and judgment questions

implementation

The built-in serialization in SprintBoot is using Jackson. If you look at the documentation, Jackson provides @jsonTypeInfo and @JsonSubtypes annotations, which can be used together to specify the specific subclass type to be used in the instantiation based on the specified field values

The actual code for these classes is as follows:

@Data
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "type",
        visible = true)
@JsonSubTypes({
        @JsonSubTypes.Type(value = SingleChoiceQuestion.class, name = Question.SINGLE_CHOICE),
        @JsonSubTypes.Type(value = MultipleChoiceQuestion.class, name = Question.MULTIPLE_CHOICE),
        @JsonSubTypes.Type(value = TrueOrFalseQuestion.class, name = Question.TRUE_OR_FALSE),
})
public abstract class Question {

    protected static final String SINGLE_CHOICE = "single_choice";
    protected static final String MULTIPLE_CHOICE = "multiple_choice";
    protected static final String TRUE_OR_FALSE = "true_or_false";

    protected String type;
    protected String content;
    protected String answer;

    protected boolean isCorrect(String answer) {
        return this.answer.equals(answer); }}Copy the code

TrueOrFalseQuestion:

@Data
@EqualsAndHashCode(callSuper = true)
public class TrueOrFalseQuestion extends Question {

    public TrueOrFalseQuestion(a) {
        this.type = TRUE_OR_FALSE; }}Copy the code

A ChoiceQuestion

@Data
@EqualsAndHashCode(callSuper = true)
public abstract class ChoiceQuestion extends Question {

    private List<Option> options;

    @Data
    public static class Option {
        private String code;
        privateString content; }}Copy the code

SingleChoiceQuestion:

@Data
@EqualsAndHashCode(callSuper = true)
public class SingleChoiceQuestion extends ChoiceQuestion {

    public SingleChoiceQuestion(a) {
        this.type = SINGLE_CHOICE; }}Copy the code

MultipleChoiceQuestion:

@Data
@EqualsAndHashCode(callSuper = true)
public class MultipleChoiceQuestion extends ChoiceQuestion {

    public MultipleChoiceQuestion(a) {
        this.type = MULTIPLE_CHOICE;
    }

    @Override
    public void setAnswer(String answer) {
        this.answer = sortString(answer);
    }

    @Override
    public boolean isCorrect(String answer) {
        return this.answer.equals(sortString(answer));
    }

    private String sortString(String str) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        returnString.valueOf(chars); }}Copy the code

test

We can use @requestBody to pass in an Exam object and return the result:

@RequestMapping(value = "/exam", method = RequestMethod.POST)
public List<String> parseExam(@RequestBody Exam exam) {
    List<String> results = new ArrayList<>();
    results.add(String.format("Parsed an exam, name = %s", exam.getName()));
    results.add(String.format("Exam has %s questions", exam.getQuestions().size())) 
    
    List<String> types = new ArrayList<>();
    for (Question question : exam.getQuestions()) {
        types.add(question.getType());
    }
    results.add(String.format("Questions types: %s", types.toString()));
    return results;
}
Copy the code

Project run, call the interface test:

Curl -x POST \ http://127.0.0.1:8080/exam/ \ -h 'the content-type: application/json \ - d' {" name ":" an exam ", "the questions" : [{" type ":" single_choice ", "content" : "single topic selection", "options" : [{" code ":" A ", "content" : "option A"}, {" code ":" B ", "content" : "Option B"}], "answer" : "A"}, {" type ":" multiple_choice ", "content" : "multiple choice", "options" : [{" code ":" A ", "content" : "Option A"}, {" code ":" B ", "content" : "option B"}], "answer" : "AB"}, {" type ":" true_or_false ", "content" : "true or false" and "answer" : "True" }] }'Copy the code

The interface returns the following:

[
    "Parsed an exam, name = a test"."Exam has 3 questions"."Questions types: [single_choice, multiple_choice, true_or_false]"
]
Copy the code

Here, different types of question and type fields can be correctly read, indicating that the class corresponding to the specific subclass was indeed called to instantiate in the deserialization process.