The idea is to implement an object, the JSONWriter
, that is able to convert an object to a
JSON text.
A JSONWriter
is able to convert
- basic JSON type like boolean, int or String
- can be configured to handle specific type like
MonthDay
ofjava.time
- recursive types, types composed of other types, likes Java Beans or records
Here is an example of a Person
defined as a record, with the Address
defined as a bean.
class Address {
private boolean international;
public boolean isInternational() {
return international;
}
}
record Person(@JSONProperty("birth-day") MonthDay birthday, Address address) { }
We can create a JSONWriter
, configure it to use a user defined format for instances of the class MonthDay
and calls toJSON()
to get the corresponding JSON text.
var writer = new JSONWriter();
writer.configure(MonthDay.class, monthDay -> writer.toJSON(monthDay.getMonth() + "-" + monthDay.getDayOfMonth()));
var person = new Person(MonthDay.of(4, 17), new Address());
var json = writer.toJSON(person); // {"birth-day": "APRIL-17", "address": {"international": false}}
The unit tests are in JSONWriterTest.java
-
Create the class
JSONWriter
and adds the methodtoJSON()
that works only with JSON primitive values,null
,true
,false
, any integers or doubles and strings. You can use a switch on type for that. Then check that the tests in the nested class "Q1" all pass. -
Adds the support of Java Beans by modifying
toJSON()
to get theBeanInfo
. Get the properties from it and use a stream with acollect(Collectors.joining())
to add the '{' and '}' and separate the values by a comma. Then check that the tests in the nested class "Q2" all pass.Note: the method
Utils.beanInfo()
already provides a way to get theBeanInfo
of a class. the methodUtils.invoke()
deals with the exception correctly when calling aMethod
. -
The problem with the current solution is that the
BeanInfo
and the properties are computed each times even if the properties of a class are always the same. The idea is to declare a ClassValue that caches an array of properties for a class. So modify the methodtoJSON()
to use aClassValue<PropertyDescriptor[]>
. All the tests from the previous questions should still pass. -
We can cache more values, by example the property name and the getter are always the same for a pair of key/value. We can observe that from the JSONWriter POV, there are two kinds of type,
- either it's a primitive those only need the object to generate the JSON text
- or it's a bean type, those need the object, and the writer to recursively call
writer.toJSON()
on the properties Thus to represent the computation, we can declare a private functional interfaceGenerator
that takes aJSONWriter
and anObject
as parameter.
private interface Generator { String generate(JSONWriter writer, Object bean); }
Change your code to use
ClassValue<Generator>
instead of aClassValue<PropertyDescriptor[]>
, and modify the implementation of the methodtoJSON()
accordingly. All the tests from the previous questions should still pass. -
Adds a method
configure()
that takes aClass
and a lambda that takes an instance of that class and returns a string and modifytoJSON()
to work with instances of the configured classes. Internally, a HashMap that associates a class to the computation of the JSON text using the lambda. Then check that the tests in the nested class "Q5" all pass.Note: the lambda takes a value and returns a value thus it can be typed by a
java.util.function.Function
. The type of the class, and the type of the first parameter of the lambda are the same, you need to introduce a type parameter for that. Exactly the type of the first parameter of the lambda is a super type of the type of the class. -
JSON keys can use any identifier not only the ones that are valid in Java. For that, we introduce an annotation
@JSONProperty
defined like this@Retention(RUNTIME) @Target({METHOD, RECORD_COMPONENT}) public @interface JSONProperty { String value(); }
To support that, add a check if the getter is annotated with the annotation
@JSONProperty
and in that case, use the name provided by the annotation instead of the name of the property. Then check that the tests in the nested class "Q6" all pass -
Modify the code to support not only Java beans but also records by refactoring your code to have two private methods that takes a Class and returns either the properties of the bean or the properties of the records.
private static List<PropertyDescriptor> beanProperties(Class<?> type) { // TODO } private static List<PropertyDescriptor> recordProperties(Class<?> type) { // TODO }
Change the code so
toJSON()
works with both records and beans. Then check that the tests in the nested class "Q1" all pass