Skip to content

[#13645] Adopt Jackson JSON library#13688

Open
iZUMi-kyouka wants to merge 61 commits intoTEAMMATES:masterfrom
iZUMi-kyouka:migrate/gson-to-jackson
Open

[#13645] Adopt Jackson JSON library#13688
iZUMi-kyouka wants to merge 61 commits intoTEAMMATES:masterfrom
iZUMi-kyouka:migrate/gson-to-jackson

Conversation

@iZUMi-kyouka
Copy link
Copy Markdown
Contributor

@iZUMi-kyouka iZUMi-kyouka commented Mar 28, 2026

Fixes #13645

Outline of Solution

  • Migrate JsonUtils to use Jackson
  • Use JsonSubtypes annotation to replace custom serialiser / deserialiser for polymorphic entities like FeedbackQuestionDetails, FeedbackResponseDetails, and LogDetails
  • Create private no-args constructor for entities classes where the constructor is unsuitable i.e. it does not simply set the fields to the given parameters
  • Create private constructors with all final fields to allow creation of objects where some fields are final
  • Use @JsonCreator annotations for classes where the existing constructor is suitable to create the object on deserialisation

Issues

  • Currently, the typescript generator gradle plugin generates the correct API output interface as expected by the impls in the frontend where each FQDetail subclass has a property questionType of type FeedbackQuestionType.
  • However, when we use the Jackson annotation, the typescript generator generates the interface for FQDetails and FRDetails using string union as the type of questionType, requiring some non-trivial amount of changes to the frontend which should be done in another PR if required.
    • Other alternatives such as forcing the generator to keep generating it as FeedbackQuestionType enum type is not possible
    • Another alternative is to perform manual string replacement after ./gradlew generateTypes but this seems brittle and ugly
  • Some testing data bundles contain unknown properties that are only present in DTO and but not the entity itself. This results in errors in Jackson deserialisation. The temporary fix is to follow Gson behaviour of ignoring unknown properties: mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);, but I think this should be removed in future for security
  • Some inconsistencies in how polymorphic type is handled e.g. @JsonSubtypes and @JsonTypeInfo annotation for LogDetails vs custom serialiser and deserialiser for FeedbackQuestion and FeedbackResponse. For both FQ and FR, this is required since the type info is nested inside the FQDetails and FRDetails and there is no way to encode this information using Jackson's annotations. Unless we flatten it, which not sure is feasible and shold be in another PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 13 out of 14 changed files in this pull request and generated no comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 47 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/main/java/teammates/ui/request/MarkNotificationAsReadRequest.java:16

  • MarkNotificationAsReadRequest uses an @JsonCreator constructor with Long endTimestamp but assigns it to a primitive long. If the JSON omits endTimestamp (or sets it to null), Jackson will pass null and this will NPE during unboxing, resulting in a 500 instead of a validation-driven 400. Consider taking a primitive long in the creator, or explicitly handling null (e.g., default to 0 so validate() can fail cleanly).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 19 to 30
String expectedQuestionDetailsJson = "{\n"
+ " \"shouldAllowRichText\": true,\n"
+ " \"questionType\": \"TEXT\",\n"
+ " \"questionText\": \"Question text.\"\n"
+ " \"questionText\": \"Question text.\",\n"
+ " \"shouldAllowRichText\": true\n"
+ "}";

assertEquals(expectedQuestionDetailsJson, JsonUtils.toJson(qd));

expectedQuestionDetailsJson = "{\"shouldAllowRichText\":true,\"questionType\":\"TEXT\","
+ "\"questionText\":\"Question text.\"}";
expectedQuestionDetailsJson = "{\"questionType\":\"TEXT\",\"questionText\":\"Question text.\","
+ "\"shouldAllowRichText\":true}";

assertEquals(expectedQuestionDetailsJson, JsonUtils.toCompactJson(qd));
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert exact JSON string output, which is brittle with Jackson (field order/pretty-printing can change across versions or mapper settings). To make the test stable, consider asserting JSON structural equality instead (e.g., parse both expected/actual into a JSON tree and compare) rather than comparing raw strings.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@iZUMi-kyouka iZUMi-kyouka Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps can be discussed in another issue. This is just a fix to align with Jackson's parent class field-first ordering.

- Disable default of sorting attributes alphabetically
- Remove explicit registration of paramnames and javatime module
- Update custom pretty printer to new API
- Update custom serde to new API
- Update annotation introspector to new API
# Conflicts:
#	src/main/java/teammates/ui/output/DeadlineExtensionData.java
#	src/main/java/teammates/ui/output/FeedbackSessionData.java
#	src/main/java/teammates/ui/output/FeedbackSessionsData.java
#	src/main/java/teammates/ui/request/MarkNotificationAsReadRequest.java
#	src/test/java/teammates/test/AbstractBackDoor.java
… of Jackson 3 in project.

- Without this, Jackson 3 will fail when ./gradlew generateTypes run since it will use jackson-annotations 2.14 brought by the ts-generator which lacks some enum constants required by Jackson 3.
…regarding Jackson's implicit default behaviour
@iZUMi-kyouka
Copy link
Copy Markdown
Contributor Author

iZUMi-kyouka commented Apr 6, 2026

Summary of Changes

  • Upgrade Jackson to v3 (include updating some imports to tools.jackson from com.fasterxml.jackson)
  • Upgrade cz.habarta.typescript.generator to 4.0.0 (just nice that it was released yesterday, and now it fully supports Jackson 3 too)
    • ISSUE: There is some issue/bug with how they implement the Jackson3Config (see build.gradle comment) as 4.0 is quite new, but I think this is still better than not upgrading as 4.0 supports Jackson 3 so there is no need to force it to use jackson-annotations 2.21, which was needed previously
  • Configure ts-generator to always output types annotated with Jackson polymorphic annotation to TS enum instead of discriminated union
  • Update JsonUtils to match the new Jackson 3 API, notably disabling of the default alphabetical sorting of serialised property, and removal of explicit registration of ParameterNamesModule and JavaTimeModule as they are included in by default
  • Jackson 3 now inherits its JacksonException from RuntimeException, rendering the custom JsonException redundant. While this custom exception provides flexibility of changing Json library in future, this seems unlikely and I think it's better to use Jackson's exception type instead
  • Explicitly set JsonCreator mode to PROPERTIES in entities with single-arg constructors as an intent to reduce confusion regarding Jackson's default behaviour
  • Centralise handling of custom behaviour to not require the subtype discriminator type when deserializing an entity of a polymorphic subtype in JsonUtils using mixin in a custom module instead of annotating all polymorphic subtype classes with JsonTypeInfo.Id.NONE
  • Tidy up JsonUtils-buildMapper to use builder pattern more, and use a custom module to aggregate registration of all custom serializers and deserializers

@iZUMi-kyouka
Copy link
Copy Markdown
Contributor Author

iZUMi-kyouka commented Apr 6, 2026

One thing that comes to mind is that currently not all entity in ui/output is ready for Jackson deserialisation since some lack private no-args constructor while others with suitable constructors need to be annotated.

Adding these only when it becomes required i.e. when tests actually deserialise them reduce clutter but also means each time new tests that possibly deserialise one of these undeserialisable entity are written, then the annotation need to be added.

Just wondering if this should be made into a separate issue to make all of them ready to be deserialised.

@iZUMi-kyouka iZUMi-kyouka force-pushed the migrate/gson-to-jackson branch from c8eb160 to 81061b0 Compare April 7, 2026 15:19
@iZUMi-kyouka iZUMi-kyouka force-pushed the migrate/gson-to-jackson branch from 81061b0 to fe5078c Compare April 7, 2026 16:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 50 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/main/java/teammates/common/datatransfer/logs/FeedbackSessionLogType.java:17

  • FeedbackSessionLogType no longer has @JsonValue on the label, which will change JSON serialization from label values (e.g., "access", "view result") to enum names (e.g., "ACCESS", "VIEW_RESULT"). This is likely to break callers that pass/expect the label form (e.g., CreateFeedbackSessionLogAction parses fsltype via valueOfLabel) and may also alter generated TypeScript enum values. Either restore @JsonValue (or add a @JsonValue getter returning label) or update parsing/clients to consistently use enum names instead of labels.
    // CHECKSTYLE.ON:JavadocVariable

    private final String label;

    FeedbackSessionLogType(String label) {
        this.label = label;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@iZUMi-kyouka iZUMi-kyouka requested review from samuelfangjw and removed request for samuelfangjw April 7, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adopt Jackson JSON library

3 participants