Skip to content

Commit bb3a0d7

Browse files
committed
Add ResultMixIn for improved JSON serialization of Result types
1 parent 4f9cad3 commit bb3a0d7

File tree

6 files changed

+72
-88
lines changed

6 files changed

+72
-88
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,13 +782,13 @@ Set `io.github.raniagus.javalidation.flatten-errors: true`:
782782
**Ok variant:**
783783
```java
784784
Result<String> result = Result.ok("success");
785-
// Serializes to: {"ok": true, "value": "success"}
785+
// Serializes to: {"ok": "true", "value": "success"}
786786
```
787787

788788
**Err variant:**
789789
```java
790790
Result<String> result = Result.err("email", "Invalid format");
791-
// Serializes to: {"ok": false, "errors": {"rootErrors": [], "fieldErrors": {"email": ["Invalid format"]}}}
791+
// Serializes to: {"ok": "false", "errors": {"rootErrors": [], "fieldErrors": {"email": ["Invalid format"]}}}
792792
```
793793

794794
**Nested in API responses:**

javalidation-jackson/src/main/java/io/github/raniagus/javalidation/jackson/JavalidationModule.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* This module registers custom serializers to control how validation types are serialized to JSON.
1515
* By default, it uses:
1616
* <ul>
17-
* <li>{@link ResultSerializer} for {@link Result} - discriminated union with {@code ok} boolean field</li>
17+
* <li>{@link ResultMixIn} for {@link Result} - discriminated union with {@code ok} boolean field</li>
1818
* <li>{@link TemplateStringSerializer} for {@link TemplateString} - formats templates using {@link TemplateStringFormatter}</li>
1919
* <li>{@link ValidationErrorsMixIn} for {@link ValidationErrors} - structures errors as {@code {root: [...], fields: {...}}}</li>
2020
* </ul>
@@ -40,36 +40,47 @@
4040
* .build();
4141
* }</pre>
4242
*
43-
* @see ResultSerializer
4443
* @see TemplateStringSerializer
4544
* @see FlattenedErrorsSerializer
4645
* @see ValidationErrorsMixIn
4746
*/
4847
public class JavalidationModule extends SimpleModule {
49-
private final boolean useDefaultSerializer;
48+
private final boolean useDefaultResultSerializer;
49+
private final boolean useDefaultErrorSerializer;
5050

5151
private JavalidationModule(
52-
ValueSerializer<Result<?>> resultSerializer,
52+
@Nullable ValueSerializer<Result<?>> resultSerializer,
5353
ValueSerializer<TemplateString> templateStringSerializer,
5454
@Nullable ValueSerializer<ValidationErrors> validationErrorsSerializer
5555
) {
5656
super(JavalidationModule.class.getSimpleName());
5757

58-
addSerializer(resultSerializer);
58+
if (resultSerializer != null) {
59+
addSerializer(resultSerializer);
60+
this.useDefaultResultSerializer = false;
61+
} else {
62+
this.useDefaultResultSerializer = true;
63+
}
64+
5965
addSerializer(TemplateString.class, templateStringSerializer);
6066

6167
if (validationErrorsSerializer != null) {
6268
addSerializer(ValidationErrors.class, validationErrorsSerializer);
63-
this.useDefaultSerializer = false;
69+
this.useDefaultErrorSerializer = false;
6470
} else {
65-
this.useDefaultSerializer = true;
71+
this.useDefaultErrorSerializer = true;
6672
}
6773
}
6874

6975
@Override
7076
public void setupModule(SetupContext context) {
7177
super.setupModule(context);
72-
if (useDefaultSerializer) {
78+
if (useDefaultResultSerializer) {
79+
context.setMixIn(Result.class, ResultMixIn.class);
80+
context.setMixIn(Result.Ok.class, ResultMixIn.OkMixin.class);
81+
context.setMixIn(Result.Err.class, ResultMixIn.ErrMixin.class);
82+
}
83+
if (useDefaultErrorSerializer) {
7384
context.setMixIn(ValidationErrors.class, ValidationErrorsMixIn.class);
7485
}
7586
}
@@ -104,7 +115,7 @@ public static Builder builder() {
104115
* @see #withFlattenedErrors()
105116
*/
106117
public static class Builder {
107-
private ValueSerializer<Result<?>> resultSerializer = new ResultSerializer();
118+
private @Nullable ValueSerializer<Result<?>> resultSerializer;
108119
private ValueSerializer<TemplateString> templateStringSerializer = new TemplateStringSerializer();
109120
private @Nullable ValueSerializer<ValidationErrors> validationErrorsSerializer;
110121

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.github.raniagus.javalidation.jackson;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import io.github.raniagus.javalidation.Result;
7+
import io.github.raniagus.javalidation.ValidationErrors;
8+
import org.jspecify.annotations.Nullable;
9+
10+
@JsonTypeInfo(
11+
use = JsonTypeInfo.Id.NAME,
12+
include = JsonTypeInfo.As.PROPERTY,
13+
property = "ok"
14+
)
15+
@JsonSubTypes({
16+
@JsonSubTypes.Type(value = Result.Ok.class, name = "true"),
17+
@JsonSubTypes.Type(value = Result.Err.class, name = "false")
18+
})
19+
public interface ResultMixIn {
20+
21+
interface OkMixin<T extends @Nullable Object> {
22+
T value();
23+
24+
@JsonIgnore
25+
ValidationErrors getErrors();
26+
27+
@JsonIgnore
28+
T getOrThrow();
29+
}
30+
31+
interface ErrMixin {
32+
ValidationErrors errors();
33+
34+
@JsonIgnore
35+
Object getOrThrow();
36+
}
37+
}

javalidation-jackson/src/main/java/io/github/raniagus/javalidation/jackson/ResultSerializer.java

Lines changed: 0 additions & 64 deletions
This file was deleted.

javalidation-jackson/src/main/java/io/github/raniagus/javalidation/jackson/ValidationErrorsMixIn.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
import java.util.List;
66
import java.util.Map;
77

8-
public abstract class ValidationErrorsMixIn {
8+
public interface ValidationErrorsMixIn {
99

10-
abstract List<TemplateString> rootErrors();
10+
List<TemplateString> rootErrors();
1111

12-
abstract Map<String, List<TemplateString>> fieldErrors();
12+
Map<String, List<TemplateString>> fieldErrors();
1313

1414
@JsonIgnore
15-
abstract boolean isEmpty();
15+
boolean isEmpty();
1616

1717
@JsonIgnore
18-
abstract boolean isNotEmpty();
18+
boolean isNotEmpty();
1919
}

javalidation-jackson/src/test/java/io/github/raniagus/javalidation/jackson/ResultSerializerTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ void givenOkWithString_whenSerialize_thenWritesTypeAndValue() {
3030
String json = mapper.writeValueAsString(result);
3131

3232
assertThat(json).isEqualTo("""
33-
{"ok":true,"value":"hello"}\
33+
{"ok":"true","value":"hello"}\
3434
""");
3535
}
3636

@@ -41,7 +41,7 @@ void givenOkWithInteger_whenSerialize_thenWritesTypeAndValue() {
4141
String json = mapper.writeValueAsString(result);
4242

4343
assertThat(json).isEqualTo("""
44-
{"ok":true,"value":42}\
44+
{"ok":"true","value":42}\
4545
""");
4646
}
4747

@@ -52,7 +52,7 @@ void givenOkWithNull_whenSerialize_thenWritesTypeAndNullValue() {
5252
String json = mapper.writeValueAsString(result);
5353

5454
assertThat(json).isEqualTo("""
55-
{"ok":true,"value":null}\
55+
{"ok":"true","value":null}\
5656
""");
5757
}
5858

@@ -64,7 +64,7 @@ record Person(String name, int age) {}
6464
String json = mapper.writeValueAsString(result);
6565

6666
assertThat(json).isEqualTo("""
67-
{"ok":true,"value":{"name":"Alice","age":30}}\
67+
{"ok":"true","value":{"name":"Alice","age":30}}\
6868
""");
6969
}
7070

@@ -77,7 +77,7 @@ void givenErrWithRootError_whenSerialize_thenWritesTypeAndErrors() {
7777
String json = mapper.writeValueAsString(result);
7878

7979
assertThat(json).isEqualTo("""
80-
{"errors":{"rootErrors":["Invalid input"],"fieldErrors":{}},"ok":false}\
80+
{"ok":"false","errors":{"rootErrors":["Invalid input"],"fieldErrors":{}}}\
8181
""");
8282
}
8383

@@ -88,7 +88,7 @@ void givenErrWithFieldError_whenSerialize_thenWritesTypeAndErrors() {
8888
String json = mapper.writeValueAsString(result);
8989

9090
assertThat(json).isEqualTo("""
91-
{"errors":{"rootErrors":[],"fieldErrors":{"email":["Invalid format"]}},"ok":false}\
91+
{"ok":"false","errors":{"rootErrors":[],"fieldErrors":{"email":["Invalid format"]}}}\
9292
""");
9393
}
9494

@@ -104,7 +104,7 @@ void givenErrWithMultipleErrors_whenSerialize_thenWritesAllErrors() {
104104
String json = mapper.writeValueAsString(result);
105105

106106
assertThat(json).isEqualTo("""
107-
{"errors":{"rootErrors":["Global error"],"fieldErrors":{"age":["Must be positive"],"name":["Required"]}},"ok":false}\
107+
{"ok":"false","errors":{"rootErrors":["Global error"],"fieldErrors":{"age":["Must be positive"],"name":["Required"]}}}\
108108
""");
109109
}
110110

@@ -118,7 +118,7 @@ record Response(String id, Result<String> result) {}
118118
String json = mapper.writeValueAsString(response);
119119

120120
assertThat(json).isEqualTo("""
121-
{"id":"123","result":{"ok":true,"value":"success"}}\
121+
{"id":"123","result":{"ok":"true","value":"success"}}\
122122
""");
123123
}
124124

0 commit comments

Comments
 (0)