0
# Custom Serialization Framework
1
2
JSON-Smart provides a powerful extension framework through JsonReader and JsonWriter interfaces that enables custom serialization and deserialization logic for any Java type.
3
4
## Overview
5
6
The customization framework allows you to:
7
8
- Define custom serialization logic for any Java class
9
- Create custom deserialization mappers for complex types
10
- Configure field name mapping between JSON and Java
11
- Handle special data types (dates, enums, custom objects)
12
- Implement domain-specific JSON transformations
13
14
## JsonWriterI - Custom Serialization
15
16
### Interface Definition
17
18
```java { .api }
19
public interface JsonWriterI<T> {
20
<E extends T> void writeJSONString(E value, Appendable out, JSONStyle compression) throws IOException;
21
}
22
```
23
24
Implement custom JSON serialization for any type.
25
26
### Basic Custom Writer
27
28
```java
29
import net.minidev.json.reader.JsonWriterI;
30
import net.minidev.json.JSONStyle;
31
import java.time.LocalDate;
32
import java.time.format.DateTimeFormatter;
33
34
// Custom writer for LocalDate
35
public class LocalDateWriter implements JsonWriterI<LocalDate> {
36
private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;
37
38
@Override
39
public <E extends LocalDate> void writeJSONString(E date, Appendable out, JSONStyle compression)
40
throws IOException {
41
if (date == null) {
42
out.append("null");
43
} else {
44
compression.writeString(out, date.format(formatter));
45
}
46
}
47
}
48
49
// Register the custom writer
50
JSONValue.registerWriter(LocalDate.class, new LocalDateWriter());
51
52
// Use with automatic serialization
53
LocalDate today = LocalDate.now();
54
String json = JSONValue.toJSONString(today); // "2023-10-15"
55
56
// Works in objects too
57
JSONObject event = new JSONObject()
58
.appendField("name", "Meeting")
59
.appendField("date", today);
60
String eventJson = JSONValue.toJSONString(event); // {"name":"Meeting","date":"2023-10-15"}
61
```
62
63
### Complex Object Writer
64
65
```java
66
// Custom writer for a complex business object
67
public class PersonWriter implements JsonWriterI<Person> {
68
69
@Override
70
public <E extends Person> void writeJSONString(E person, Appendable out, JSONStyle compression)
71
throws IOException {
72
73
out.append('{');
74
75
boolean first = true;
76
77
// Write ID
78
if (!first) out.append(',');
79
JSONObject.writeJSONKV("id", person.getId(), out, compression);
80
first = false;
81
82
// Write full name (computed from first + last)
83
if (!first) out.append(',');
84
String fullName = person.getFirstName() + " " + person.getLastName();
85
JSONObject.writeJSONKV("fullName", fullName, out, compression);
86
87
// Write age only if adult
88
if (person.getAge() >= 18) {
89
out.append(',');
90
JSONObject.writeJSONKV("age", person.getAge(), out, compression);
91
}
92
93
// Write roles as array
94
if (!person.getRoles().isEmpty()) {
95
out.append(',');
96
JSONObject.writeJSONKV("roles", person.getRoles(), out, compression);
97
}
98
99
out.append('}');
100
}
101
}
102
103
// Usage
104
JSONValue.registerWriter(Person.class, new PersonWriter());
105
106
Person person = new Person(1, "John", "Doe", 25, Arrays.asList("admin", "user"));
107
String json = JSONValue.toJSONString(person);
108
// {"id":1,"fullName":"John Doe","age":25,"roles":["admin","user"]}
109
```
110
111
### Enum Writer with Custom Values
112
113
```java
114
public enum Status {
115
ACTIVE, INACTIVE, PENDING, SUSPENDED
116
}
117
118
public class StatusWriter implements JsonWriterI<Status> {
119
120
@Override
121
public <E extends Status> void writeJSONString(E status, Appendable out, JSONStyle compression)
122
throws IOException {
123
if (status == null) {
124
out.append("null");
125
return;
126
}
127
128
String value = switch (status) {
129
case ACTIVE -> "A";
130
case INACTIVE -> "I";
131
case PENDING -> "P";
132
case SUSPENDED -> "S";
133
};
134
135
compression.writeString(out, value);
136
}
137
}
138
139
// Register and use
140
JSONValue.registerWriter(Status.class, new StatusWriter());
141
142
Status status = Status.ACTIVE;
143
String json = JSONValue.toJSONString(status); // "A"
144
```
145
146
## JsonReader - Registry and Factory
147
148
The JsonReader class manages the registry of custom deserializers and provides factory methods.
149
150
### JsonReader Class
151
152
```java { .api }
153
public class JsonReader {
154
public JsonReaderI<JSONAwareEx> DEFAULT;
155
public JsonReaderI<JSONAwareEx> DEFAULT_ORDERED;
156
157
public JsonReader();
158
public <T> void remapField(Class<T> type, String fromJson, String toJava);
159
public <T> void registerReader(Class<T> type, JsonReaderI<T> mapper);
160
public <T> JsonReaderI<T> getMapper(Class<T> type);
161
public <T> JsonReaderI<T> getMapper(Type type);
162
}
163
```
164
165
### Global Registration Methods
166
167
```java { .api }
168
// In JSONValue class
169
public static <T> void registerReader(Class<T> type, JsonReaderI<T> mapper);
170
public static <T> void remapField(Class<T> type, String fromJson, String toJava);
171
```
172
173
Register custom deserializers globally.
174
175
```java
176
// Register custom reader for LocalDate
177
JSONValue.registerReader(LocalDate.class, new LocalDateReader());
178
179
// Configure field name mapping
180
JSONValue.remapField(Person.class, "first_name", "firstName");
181
JSONValue.remapField(Person.class, "last_name", "lastName");
182
```
183
184
## JsonReaderI - Custom Deserialization
185
186
### Abstract Base Class
187
188
```java { .api }
189
public abstract class JsonReaderI<T> {
190
public JsonReaderI(JsonReader base);
191
192
// Override as needed:
193
public JsonReaderI<?> startObject(String key) throws ParseException, IOException;
194
public JsonReaderI<?> startArray(String key) throws ParseException, IOException;
195
public void setValue(Object current, String key, Object value) throws ParseException, IOException;
196
public Object getValue(Object current, String key);
197
public Type getType(String key);
198
public void addValue(Object current, Object value) throws ParseException, IOException;
199
public Object createObject();
200
public Object createArray();
201
public abstract T convert(Object current);
202
}
203
```
204
205
### Simple Type Reader
206
207
```java
208
import net.minidev.json.writer.JsonReaderI;
209
import net.minidev.json.writer.JsonReader;
210
import java.time.LocalDate;
211
import java.time.format.DateTimeFormatter;
212
213
public class LocalDateReader extends JsonReaderI<LocalDate> {
214
215
public LocalDateReader() {
216
super(new JsonReader());
217
}
218
219
@Override
220
public LocalDate convert(Object current) {
221
if (current == null) {
222
return null;
223
}
224
225
if (current instanceof String) {
226
String dateStr = (String) current;
227
try {
228
return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
229
} catch (Exception e) {
230
// Try alternative formats
231
try {
232
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("MM/dd/yyyy"));
233
} catch (Exception e2) {
234
throw new RuntimeException("Cannot parse date: " + dateStr, e2);
235
}
236
}
237
}
238
239
throw new RuntimeException("Cannot convert " + current.getClass() + " to LocalDate");
240
}
241
}
242
243
// Register and use
244
JSONValue.registerReader(LocalDate.class, new LocalDateReader());
245
246
// Parse various date formats
247
LocalDate date1 = JSONValue.parse("\"2023-10-15\"", LocalDate.class);
248
LocalDate date2 = JSONValue.parse("\"10/15/2023\"", LocalDate.class);
249
```
250
251
### Complex Object Reader
252
253
```java
254
public class PersonReader extends JsonReaderI<Person> {
255
256
public PersonReader() {
257
super(new JsonReader());
258
}
259
260
@Override
261
public Person convert(Object current) {
262
if (!(current instanceof Map)) {
263
throw new RuntimeException("Expected JSON object for Person");
264
}
265
266
@SuppressWarnings("unchecked")
267
Map<String, Object> map = (Map<String, Object>) current;
268
269
// Extract fields with defaults and validation
270
Integer id = getAsInteger(map, "id");
271
if (id == null) {
272
throw new RuntimeException("Person ID is required");
273
}
274
275
String firstName = getAsString(map, "firstName", "");
276
String lastName = getAsString(map, "lastName", "");
277
Integer age = getAsInteger(map, "age", 0);
278
279
// Handle roles array
280
List<String> roles = new ArrayList<>();
281
Object rolesObj = map.get("roles");
282
if (rolesObj instanceof List) {
283
@SuppressWarnings("unchecked")
284
List<Object> rolesList = (List<Object>) rolesObj;
285
for (Object role : rolesList) {
286
if (role instanceof String) {
287
roles.add((String) role);
288
}
289
}
290
}
291
292
return new Person(id, firstName, lastName, age, roles);
293
}
294
295
private String getAsString(Map<String, Object> map, String key, String defaultValue) {
296
Object value = map.get(key);
297
return value instanceof String ? (String) value : defaultValue;
298
}
299
300
private Integer getAsInteger(Map<String, Object> map, String key) {
301
return getAsInteger(map, key, null);
302
}
303
304
private Integer getAsInteger(Map<String, Object> map, String key, Integer defaultValue) {
305
Object value = map.get(key);
306
if (value instanceof Number) {
307
return ((Number) value).intValue();
308
}
309
if (value instanceof String) {
310
try {
311
return Integer.parseInt((String) value);
312
} catch (NumberFormatException e) {
313
return defaultValue;
314
}
315
}
316
return defaultValue;
317
}
318
}
319
320
// Register and use
321
JSONValue.registerReader(Person.class, new PersonReader());
322
323
String json = """
324
{
325
"id": 1,
326
"firstName": "John",
327
"lastName": "Doe",
328
"age": "25",
329
"roles": ["admin", "user"]
330
}
331
""";
332
333
Person person = JSONValue.parse(json, Person.class);
334
```
335
336
### Collection Reader
337
338
```java
339
import java.lang.reflect.ParameterizedType;
340
import java.lang.reflect.Type;
341
342
public class PersonListReader extends JsonReaderI<List<Person>> {
343
344
public PersonListReader() {
345
super(new JsonReader());
346
}
347
348
@Override
349
public List<Person> convert(Object current) {
350
if (!(current instanceof List)) {
351
throw new RuntimeException("Expected JSON array for Person list");
352
}
353
354
@SuppressWarnings("unchecked")
355
List<Object> list = (List<Object>) current;
356
357
List<Person> persons = new ArrayList<>();
358
PersonReader personReader = new PersonReader();
359
360
for (Object item : list) {
361
Person person = personReader.convert(item);
362
persons.add(person);
363
}
364
365
return persons;
366
}
367
}
368
369
// Create custom type for registration
370
public class PersonList extends ArrayList<Person> {}
371
372
// Register and use
373
JSONValue.registerReader(PersonList.class, new PersonListReader());
374
375
String json = """
376
[
377
{"id": 1, "firstName": "Alice", "lastName": "Johnson"},
378
{"id": 2, "firstName": "Bob", "lastName": "Smith"}
379
]
380
""";
381
382
PersonList persons = JSONValue.parse(json, PersonList.class);
383
```
384
385
## Field Name Mapping
386
387
### Simple Field Mapping
388
389
```java { .api }
390
public static <T> void remapField(Class<T> type, String fromJson, String toJava);
391
```
392
393
Map JSON field names to Java field names.
394
395
```java
396
public class User {
397
private String firstName;
398
private String lastName;
399
private String emailAddress;
400
401
// getters and setters...
402
}
403
404
// Configure field mapping
405
JSONValue.remapField(User.class, "first_name", "firstName");
406
JSONValue.remapField(User.class, "last_name", "lastName");
407
JSONValue.remapField(User.class, "email", "emailAddress");
408
409
// Now JSON with snake_case is automatically mapped
410
String json = """
411
{
412
"first_name": "John",
413
"last_name": "Doe",
414
"email": "john@example.com"
415
}
416
""";
417
418
User user = JSONValue.parse(json, User.class);
419
System.out.println(user.getFirstName()); // "John"
420
```
421
422
### Advanced Field Mapping in Custom Reader
423
424
```java
425
public class FlexiblePersonReader extends JsonReaderI<Person> {
426
427
public FlexiblePersonReader() {
428
super(new JsonReader());
429
}
430
431
@Override
432
public Person convert(Object current) {
433
if (!(current instanceof Map)) {
434
throw new RuntimeException("Expected JSON object");
435
}
436
437
@SuppressWarnings("unchecked")
438
Map<String, Object> map = (Map<String, Object>) current;
439
440
// Try multiple field name variations
441
Integer id = findValue(map, Integer.class, "id", "ID", "personId", "person_id");
442
String firstName = findValue(map, String.class, "firstName", "first_name", "fname", "given_name");
443
String lastName = findValue(map, String.class, "lastName", "last_name", "lname", "family_name", "surname");
444
Integer age = findValue(map, Integer.class, "age", "years", "yearsOld");
445
446
return new Person(id, firstName, lastName, age, Collections.emptyList());
447
}
448
449
@SuppressWarnings("unchecked")
450
private <T> T findValue(Map<String, Object> map, Class<T> type, String... keys) {
451
for (String key : keys) {
452
Object value = map.get(key);
453
if (value != null) {
454
if (type.isInstance(value)) {
455
return (T) value;
456
} else if (type == Integer.class && value instanceof Number) {
457
return (T) Integer.valueOf(((Number) value).intValue());
458
} else if (type == String.class) {
459
return (T) value.toString();
460
}
461
}
462
}
463
return null;
464
}
465
}
466
467
// This reader handles multiple JSON formats:
468
String json1 = """{"id": 1, "firstName": "John", "lastName": "Doe", "age": 30}""";
469
String json2 = """{"ID": 1, "first_name": "John", "family_name": "Doe", "years": 30}""";
470
String json3 = """{"personId": 1, "fname": "John", "surname": "Doe", "yearsOld": 30}""";
471
472
JSONValue.registerReader(Person.class, new FlexiblePersonReader());
473
474
Person p1 = JSONValue.parse(json1, Person.class);
475
Person p2 = JSONValue.parse(json2, Person.class);
476
Person p3 = JSONValue.parse(json3, Person.class);
477
// All produce equivalent Person objects
478
```
479
480
## JsonWriter - Serialization Registry
481
482
The JsonWriter class manages custom serializers.
483
484
### JsonWriter Class
485
486
```java { .api }
487
public class JsonWriter {
488
public JsonWriter();
489
public <T> void remapField(Class<T> type, String fromJava, String toJson);
490
public JsonWriterI<?> getWriterByInterface(Class<?> clazz);
491
public JsonWriterI getWrite(Class cls);
492
}
493
```
494
495
### Built-in Writers
496
497
```java { .api }
498
public static final JsonWriterI<JSONStreamAwareEx> JSONStreamAwareWriter;
499
```
500
501
Access built-in serialization writers.
502
503
## Advanced Customization Examples
504
505
### Polymorphic Serialization
506
507
```java
508
// Base class
509
public abstract class Shape {
510
protected String type;
511
protected String color;
512
513
// getters and setters...
514
}
515
516
public class Circle extends Shape {
517
private double radius;
518
// constructors, getters, setters...
519
}
520
521
public class Rectangle extends Shape {
522
private double width;
523
private double height;
524
// constructors, getters, setters...
525
}
526
527
// Polymorphic writer
528
public class ShapeWriter implements JsonWriterI<Shape> {
529
530
@Override
531
public <E extends Shape> void writeJSONString(E shape, Appendable out, JSONStyle compression)
532
throws IOException {
533
534
out.append('{');
535
536
// Always include type discriminator
537
JSONObject.writeJSONKV("type", shape.getClass().getSimpleName().toLowerCase(), out, compression);
538
out.append(',');
539
JSONObject.writeJSONKV("color", shape.getColor(), out, compression);
540
541
// Type-specific fields
542
if (shape instanceof Circle) {
543
Circle circle = (Circle) shape;
544
out.append(',');
545
JSONObject.writeJSONKV("radius", circle.getRadius(), out, compression);
546
547
} else if (shape instanceof Rectangle) {
548
Rectangle rect = (Rectangle) shape;
549
out.append(',');
550
JSONObject.writeJSONKV("width", rect.getWidth(), out, compression);
551
out.append(',');
552
JSONObject.writeJSONKV("height", rect.getHeight(), out, compression);
553
}
554
555
out.append('}');
556
}
557
}
558
559
// Polymorphic reader
560
public class ShapeReader extends JsonReaderI<Shape> {
561
562
public ShapeReader() {
563
super(new JsonReader());
564
}
565
566
@Override
567
public Shape convert(Object current) {
568
if (!(current instanceof Map)) {
569
throw new RuntimeException("Expected JSON object");
570
}
571
572
@SuppressWarnings("unchecked")
573
Map<String, Object> map = (Map<String, Object>) current;
574
575
String type = (String) map.get("type");
576
String color = (String) map.get("color");
577
578
switch (type) {
579
case "circle":
580
Circle circle = new Circle();
581
circle.setColor(color);
582
circle.setRadius(((Number) map.get("radius")).doubleValue());
583
return circle;
584
585
case "rectangle":
586
Rectangle rect = new Rectangle();
587
rect.setColor(color);
588
rect.setWidth(((Number) map.get("width")).doubleValue());
589
rect.setHeight(((Number) map.get("height")).doubleValue());
590
return rect;
591
592
default:
593
throw new RuntimeException("Unknown shape type: " + type);
594
}
595
}
596
}
597
598
// Register polymorphic handlers
599
JSONValue.registerWriter(Shape.class, new ShapeWriter());
600
JSONValue.registerReader(Shape.class, new ShapeReader());
601
602
// Usage
603
Circle circle = new Circle();
604
circle.setColor("red");
605
circle.setRadius(5.0);
606
607
String json = JSONValue.toJSONString(circle);
608
// {"type":"circle","color":"red","radius":5.0}
609
610
Shape parsed = JSONValue.parse(json, Shape.class);
611
// Returns Circle instance
612
```
613
614
### Custom Collection Serialization
615
616
```java
617
// Custom collection that maintains insertion order and prevents duplicates
618
public class OrderedSet<T> extends LinkedHashSet<T> {
619
// Custom collection implementation
620
}
621
622
public class OrderedSetWriter implements JsonWriterI<OrderedSet<?>> {
623
624
@Override
625
public <E extends OrderedSet<?>> void writeJSONString(E set, Appendable out, JSONStyle compression)
626
throws IOException {
627
628
// Serialize as array with metadata
629
out.append('{');
630
JSONObject.writeJSONKV("type", "orderedSet", out, compression);
631
out.append(',');
632
JSONObject.writeJSONKV("size", set.size(), out, compression);
633
out.append(',');
634
out.append("\"items\":");
635
636
// Write items as array
637
JSONArray.writeJSONString(new ArrayList<>(set), out, compression);
638
639
out.append('}');
640
}
641
}
642
643
public class OrderedSetReader extends JsonReaderI<OrderedSet<Object>> {
644
645
public OrderedSetReader() {
646
super(new JsonReader());
647
}
648
649
@Override
650
public OrderedSet<Object> convert(Object current) {
651
if (!(current instanceof Map)) {
652
throw new RuntimeException("Expected JSON object");
653
}
654
655
@SuppressWarnings("unchecked")
656
Map<String, Object> map = (Map<String, Object>) current;
657
658
String type = (String) map.get("type");
659
if (!"orderedSet".equals(type)) {
660
throw new RuntimeException("Expected orderedSet type");
661
}
662
663
Object itemsObj = map.get("items");
664
if (!(itemsObj instanceof List)) {
665
throw new RuntimeException("Expected items array");
666
}
667
668
@SuppressWarnings("unchecked")
669
List<Object> items = (List<Object>) itemsObj;
670
671
OrderedSet<Object> result = new OrderedSet<>();
672
result.addAll(items);
673
674
return result;
675
}
676
}
677
678
// Register custom collection handlers
679
JSONValue.registerWriter(OrderedSet.class, new OrderedSetWriter());
680
JSONValue.registerReader(OrderedSet.class, new OrderedSetReader());
681
```
682
683
### Conditional Serialization
684
685
```java
686
// Writer that excludes null and empty values
687
public class CleanObjectWriter implements JsonWriterI<Object> {
688
689
@Override
690
public <E> void writeJSONString(E obj, Appendable out, JSONStyle compression)
691
throws IOException {
692
693
if (obj == null) {
694
out.append("null");
695
return;
696
}
697
698
// Use reflection to get fields
699
Class<?> clazz = obj.getClass();
700
Field[] fields = clazz.getDeclaredFields();
701
702
out.append('{');
703
boolean first = true;
704
705
for (Field field : fields) {
706
field.setAccessible(true);
707
708
try {
709
Object value = field.get(obj);
710
711
// Skip null values, empty strings, empty collections
712
if (shouldSkipValue(value)) {
713
continue;
714
}
715
716
if (!first) {
717
out.append(',');
718
}
719
720
String fieldName = field.getName();
721
JSONObject.writeJSONKV(fieldName, value, out, compression);
722
first = false;
723
724
} catch (IllegalAccessException e) {
725
// Skip inaccessible fields
726
}
727
}
728
729
out.append('}');
730
}
731
732
private boolean shouldSkipValue(Object value) {
733
if (value == null) return true;
734
if (value instanceof String && ((String) value).isEmpty()) return true;
735
if (value instanceof Collection && ((Collection<?>) value).isEmpty()) return true;
736
if (value instanceof Map && ((Map<?, ?>) value).isEmpty()) return true;
737
return false;
738
}
739
}
740
741
// Register for specific classes that need clean serialization
742
JSONValue.registerWriter(MyDataClass.class, new CleanObjectWriter());
743
```