Deserialization of Subclasses in SpringBoot

Time:2019-9-19

target

In SpringBook interface, we usually use@RequestBodyClass annotations require deserialized objects, but when there are multiple subclasses, conventional deserialization cannot meet the requirements, such as:

We have a class Exam to represent a test paper:

@Data
public class Exam {

    private String name;
    private List<Question> questions;
}

HereQuestionIn particular, Question itself is an abstract class, which provides some general method calls. Actual subclasses include multiple-choice questions, multiple-choice questions and judgment questions.
Deserialization of Subclasses in SpringBoot

Realization

SprintBoot’s built-in serialization is using Jackson, which is available after consulting the documentation@JsonTypeInfoand@JsonSubTypesThese two annotations, used in conjunction, can specify specific subclass types used in instantiation based on the specified field values

The actual code for these classes is as follows:
The abstract base class Question:

@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);
    }
}

TrueOrFalseQuestion:

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

    public TrueOrFalseQuestion() {
        this.type = TRUE_OR_FALSE;
    }
}

Choice Question:

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

    private List<Option> options;

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

Single Choice Question:

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

    public SingleChoiceQuestion() {
        this.type = SINGLE_CHOICE;
    }
}

MultipleChoiceQuestion:

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

    public MultipleChoiceQuestion() {
        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);
        return String.valueOf(chars);
    }
}

test

Next, test it.
Defining an interface, we can use @RequestBody to pass in an Exam object and return the parsed 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;
}

The project runs and calls the interface to test:

curl -X POST \
  http://127.0.0.1:8080/exam/ \
  -H 'Content-Type: application/json' \
  -d '{
    "Name": "An exam".
    "questions": [
        {
            "type": "single_choice",
            "Content": "Selected Topics"
            "options":  [
                {
                    "code":"A",
                    "Content": "Option A"
                },{
                    "code":"B",
                    "Content": "Option B"
                }],
            "answer": "A"
        },{
            "type": "multiple_choice",
            "Content": "Multiple Subjects"
            "options":  [
                {
                    "code":"A",
                    "Content": "Option A"
                },{
                    "code":"B",
                    "Content": "Option B"
                }],
            "answer": "AB"
        },{
            "type": "true_or_false",
            "Content": "Judgment Question"
            "answer": "True"
        }]
}'

The interface returns as follows:

[
    "Parsed an exam, name = an exam"
    "Exam has 3 questions",
    "Questions types: [single_choice, multiple_choice, true_or_false]"
]

There are different types of questions, the type field can be read correctly, indicating that in the deserialization process, the corresponding classes of specific subclasses are indeed invoked to instantiate.

Recommended Today

Sharing 10 useful methods of laravel 5.8 sets

This article is forwarded from the professional laravel developer community, original link: https://learnku.com/laravel/t… In laravel, there is a very useful class for manipulating arrays, called collections. I believe that every developer of laravel has used the collection more or less, especially when operating eloquent. In this article, I will list 10 common methods. 1. Weight […]