or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdcore-api.mdcustomization.mdindex.mdnavigation.mdparsing.md

customization.mddocs/

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

```