Skip to content

Instantly share code, notes, and snippets.

@MichalBrylka
Last active January 12, 2025 18:02
Show Gist options
  • Save MichalBrylka/263f560f80d7e59212016c9046a4cc2a to your computer and use it in GitHub Desktop.
Save MichalBrylka/263f560f80d7e59212016c9046a4cc2a to your computer and use it in GitHub Desktop.
JsonAnyGetter_CustomSerdes
public static String convertMapToMapOfCode(Map<Class<?>, String> map) {
StringBuilder builder = new StringBuilder("Map<Class<?>, String> map = Map.of(\n");
map.forEach((key, value) ->
builder.append(" ")
.append(key.getName())
.append(".class, \"")
.append(value.replace("\"", "\\\""))
.append("\",\n")
);
// Remove the trailing comma and newline
builder.setLength(builder.length() - 2);
builder.append("\n);");
return builder.toString();
}
package org.example2;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class JsonAnyGetter_CustomSerdes {
public static void run() throws JsonProcessingException {
process();
//serialize();
}
private static void process() {
RuleProcessorManager manager = new RuleProcessorManager();
// Create example instances of all rules
Rule1 rule1 = new Rule1("expression1");
Rule2 rule2 = new Rule2(true);
Rule3 rule3 = new Rule3(99.99, 25);
Rule4 rule4 = new Rule4('A', true);
Rule5 rule5 = new Rule5(10, "example");
Rule6 rule6 = new Rule6(123456789L, 42.42);
Rule7 rule7 = new Rule7("message", false, true, false);
Rule8 rule8 = new Rule8(0.8, 3, false, true);
Rule9 rule9 = new Rule9("key", "value", true, false);
// Pass the rules through the manager
manager.delegateRule(rule1);
manager.delegateRule(rule2);
manager.delegateRule(rule3);
manager.delegateRule(rule4);
manager.delegateRule(rule5);
manager.delegateRule(rule6);
manager.delegateRule(rule7);
manager.delegateRule(rule8);
manager.delegateRule(rule9);
}
private static void serialize() throws JsonProcessingException {
var fieldRule = new FieldRule(
List.of("input1", "input2"),
List.of("output1", "output2"),
new Rule1("expression1")
);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String json = objectMapper.writeValueAsString(fieldRule);
System.out.println(json);
var deser = objectMapper.readValue(json, FieldRule.class);
System.out.println(deser);
}
}
final class RuleSerialization {
public static final String INPUTS = "inputs";
public static final String OUTPUTS = "outputs";
//TODO hardcode rule map and add tests to check that against reflected state
static final Map<Class<? extends Rule>, String> RULE_TYPE_MAP;
static {
List<Class<? extends Rule>> derivedClasses = new ArrayList<>();
collectPermittedSubclasses(Rule.class, derivedClasses);
RULE_TYPE_MAP = derivedClasses.stream()
.collect(Collectors.toMap(
clazz -> clazz,
clazz -> toCamelCase(clazz.getSimpleName())
));
if (RULE_TYPE_MAP.isEmpty())
throw new IllegalStateException("No rules defined");
}
private static <T> void collectPermittedSubclasses(Class<? extends T> clazz, List<Class<? extends T>> result) {
var permittedSubclasses = clazz.getPermittedSubclasses();
if (permittedSubclasses == null) return;
for (var elem : permittedSubclasses) {
var c = (Class<? extends T>) elem;
if (!c.isInterface())
result.add(c);
collectPermittedSubclasses(c, result);
}
}
private static String toCamelCase(String className) {
return Character.toLowerCase(className.charAt(0)) + className.substring(1);
}
}
class FieldRuleSerializer extends JsonSerializer<FieldRule> {
@Override
public void serialize(FieldRule value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeObjectField(RuleSerialization.INPUTS, value.getInputs());
gen.writeObjectField(RuleSerialization.OUTPUTS, value.getOutputs());
var rule = value.getMainRule();
if (rule != null) {
var rulePropertyName = RuleSerialization.RULE_TYPE_MAP.get(rule.getClass());
if (rulePropertyName == null)
throw new IllegalStateException(rule.getClass().getSimpleName() + " is not supported");
gen.writeObjectField(rulePropertyName, rule);
}
gen.writeEndObject();
}
}
class FieldRuleDeserializer extends JsonDeserializer<FieldRule> {
@Override
public FieldRule deserialize(JsonParser p, DeserializationContext context) throws IOException {
JsonNode node = p.readValueAsTree();
var listOfString = context.getTypeFactory().constructCollectionType(List.class, String.class);
List<String> inputs = node.has(RuleSerialization.INPUTS) ?
context.readTreeAsValue(node.get(RuleSerialization.INPUTS), listOfString) :
null;
List<String> outputs = node.has(RuleSerialization.OUTPUTS) ?
context.readTreeAsValue(node.get(RuleSerialization.OUTPUTS), listOfString) :
null;
Rule mainRule = null;
String ruleName = null;
for (var entry : RuleSerialization.RULE_TYPE_MAP.entrySet()) {
var name = entry.getValue();
var type = entry.getKey();
if (node.has(name)) {
mainRule = context.readTreeAsValue(node.get(name), type);
ruleName = name;
break;
}
}
//TODO tests for 0 and 2+ rule definitions
if (mainRule == null) throw JsonMappingException.from(p, "Rule definition was expected");
var fieldIterator = node.fieldNames();
while (fieldIterator.hasNext()) {
var field = fieldIterator.next();
if (!RuleSerialization.OUTPUTS.equals(field) &&
!RuleSerialization.INPUTS.equals(field) &&
!ruleName.equals(field))
throw JsonMappingException.from(p, "Only one rule definition is expected");
}
return new FieldRule(inputs, outputs, mainRule);
}
}
@lombok.Value
@JsonSerialize(using = FieldRuleSerializer.class)
@JsonDeserialize(using = FieldRuleDeserializer.class)
class FieldRule {
List<String> inputs;
List<String> outputs;
Rule mainRule;
}
sealed interface Rule permits Rule1, Rule2, Rule3, Rule4, Rule5, Rule6, CopyRule {
default String validate() {
return null;
}
}
record Rule1(String expression) implements Rule {
}
record Rule2(Boolean append) implements Rule {
}
record Rule3(Double price, Integer age) implements Rule {
}
record Rule4(Character code, Boolean isActive) implements Rule {
}
record Rule5(Integer count, String name) implements Rule {
}
record Rule6(Long id, Double value) implements Rule {
}
sealed interface CopyRule extends Rule permits Rule7, Rule8, Rule9 {
Boolean getAppend();
Boolean getPrepend();
@Override
default String validate() {
return getAppend() && getPrepend() ? "Not possible" : null;
}
}
record Rule7(String message, Boolean isError, Boolean append, Boolean prepend) implements CopyRule {
@Override
public Boolean getAppend() {
return append;
}
@Override
public Boolean getPrepend() {
return prepend;
}
}
record Rule8(Double probability, Integer attempts, Boolean append, Boolean prepend) implements CopyRule {
@Override
public Boolean getAppend() {
return append;
}
@Override
public Boolean getPrepend() {
return prepend;
}
}
record Rule9(String key, String value, Boolean append, Boolean prepend) implements CopyRule {
@Override
public Boolean getAppend() {
return append;
}
@Override
public Boolean getPrepend() {
return prepend;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>JavaExample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version> <!-- Use the latest stable version -->
</dependency>
<dependency>
<groupId>com.udojava</groupId>
<artifactId>EvalEx</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
<version>4.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
</project>
package org.example2;
public class RuleProcessorManager {
private static final RuleProcessor<Rule1> RULE1_PROCESSOR = new Rule1Processor();
private static final RuleProcessor<Rule2> RULE2_PROCESSOR = new Rule2Processor();
private static final RuleProcessor<Rule3> RULE3_PROCESSOR = new Rule3Processor();
private static final RuleProcessor<Rule4> RULE4_PROCESSOR = new Rule4Processor();
private static final RuleProcessor<Rule5> RULE5_PROCESSOR = new Rule5Processor();
private static final RuleProcessor<Rule6> RULE6_PROCESSOR = new Rule6Processor();
private static final RuleProcessor<CopyRule> COPY_RULE_PROCESSOR = new CopyRuleProcessor();
public void delegateRule(Rule rule) {
switch (rule) {
case Rule1 r -> RULE1_PROCESSOR.process(r);
case Rule2 r -> RULE2_PROCESSOR.process(r);
case Rule3 r -> RULE3_PROCESSOR.process(r);
case Rule4 r -> RULE4_PROCESSOR.process(r);
case Rule5 r -> RULE5_PROCESSOR.process(r);
case Rule6 r -> RULE6_PROCESSOR.process(r);
case CopyRule r -> COPY_RULE_PROCESSOR.process(r);
default -> throw new IllegalArgumentException("Unsupported rule type: " + rule.getClass());
}
}
}
sealed interface RuleProcessor<TRule extends Rule> permits CopyRuleProcessor, Rule1Processor, Rule2Processor, Rule3Processor, Rule4Processor, Rule5Processor, Rule6Processor {
void process(TRule rule);
}
final class Rule1Processor implements RuleProcessor<Rule1> {
@Override
public void process(Rule1 rule) {
System.out.println("Processing Rule1: " + rule);
}
}
final class Rule2Processor implements RuleProcessor<Rule2> {
@Override
public void process(Rule2 rule) {
System.out.println("Processing Rule2: " + rule);
}
}
final class Rule3Processor implements RuleProcessor<Rule3> {
@Override
public void process(Rule3 rule) {
System.out.println("Processing Rule3: " + rule);
}
}
final class Rule4Processor implements RuleProcessor<Rule4> {
@Override
public void process(Rule4 rule) {
System.out.println("Processing Rule4: " + rule);
}
}
final class Rule5Processor implements RuleProcessor<Rule5> {
@Override
public void process(Rule5 rule) {
System.out.println("Processing Rule5: " + rule);
}
}
final class Rule6Processor implements RuleProcessor<Rule6> {
@Override
public void process(Rule6 rule) {
System.out.println("Processing Rule6: " + rule);
}
}
final class CopyRuleProcessor implements RuleProcessor<CopyRule> {
@Override
public void process(CopyRule rule) {
System.out.println("Processing CopyRule: " + rule);
}
}
package org.example2;
import net.datafaker.Faker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
class RuleSerializationTest {
private static final Random random = new Random();
private static final Faker faker = new Faker(random);
/* //TODO test cases for null
Arguments.of(new FieldRule(List.of("input1"), List.of(), null), """
{
"inputs" : [ "input1" ],
"outputs" : [ ]
}"""),
Arguments.of(new FieldRule(List.of("input1", "input2"), List.of("output1"), null), """
{
"inputs" : [ "input1", "input2" ],
"outputs" : [ "output1" ]
}"""),*/
@ParameterizedTest
@MethodSource("provideFieldRuleData")
@DisplayName("test serialization and deserialization - sample data")
void testSerializationAndDeserialization_UsingProvideData(FieldRule input, String expectedJson) throws JsonProcessingException {
var objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String json = objectMapper.writeValueAsString(input);
var deserialized = objectMapper.readValue(json, FieldRule.class);
var deserialized2 = objectMapper.readValue(expectedJson, FieldRule.class);
assertThat(deserialized)
.usingRecursiveComparison()
.isEqualTo(input);
assertThat(deserialized)
.usingRecursiveComparison()
.isEqualTo(deserialized2);
assertThatJson(json).isEqualTo(expectedJson);
}
private static Stream<Arguments> provideFieldRuleData() {
return Stream.of(
Arguments.of(new FieldRule(List.of("input1", "input2"), List.of("output1"), new Rule1("expression1")), """
{
"inputs" : [ "input1", "input2" ],
"outputs" : [ "output1" ],
"rule1" : {
"expression" : "expression1"
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of("output1", "output2"), new Rule2(true)), """
{
"inputs" : [ ],
"outputs" : [ "output1", "output2" ],
"rule2" : {
"append" : true
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of(), new Rule3(10.0, 25)), """
{
"inputs" : [ "input1" ],
"outputs" : [ ],
"rule3" : {
"price" : 10.0,
"age" : 25
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule1("empty")), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule1" : {
"expression" : "empty"
}
}"""),
Arguments.of(new FieldRule(List.of("input1", "input2", "input3"), List.of("output1", "output2", "output3"), new Rule1("complex_expression")), """
{
"inputs" : [ "input1", "input2", "input3" ],
"outputs" : [ "output1", "output2", "output3" ],
"rule1" : {
"expression" : "complex_expression"
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of("output1"), new Rule3(10.0, 25)), """
{
"inputs" : [ "input1" ],
"outputs" : [ "output1" ],
"rule3" : {
"price" : 10.0,
"age" : 25
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule3(0.0, 0)), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule3" : {
"price" : 0.0,
"age" : 0
}
}"""),
Arguments.of(new FieldRule(List.of("input1", "input2"), List.of("output1"), new Rule4('A', true)), """
{
"inputs" : [ "input1", "input2" ],
"outputs" : [ "output1" ],
"rule4" : {
"code" : "A",
"isActive" : true
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule4('Z', false)), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule4" : {
"code" : "Z",
"isActive" : false
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of("output1"), new Rule5(10, "name1")), """
{
"inputs" : [ ],
"outputs" : [ "output1" ],
"rule5" : {
"count" : 10,
"name" : "name1"
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of(), new Rule5(0, "empty")), """
{
"inputs" : [ "input1" ],
"outputs" : [ ],
"rule5" : {
"count" : 0,
"name" : "empty"
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of("output1"), new Rule6(123L, 3.14)), """
{
"inputs" : [ "input1" ],
"outputs" : [ "output1" ],
"rule6" : {
"id" : 123,
"value" : 3.14
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule6(0L, 0.0)), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule6" : {
"id" : 0,
"value" : 0.0
}
}"""),
Arguments.of(new FieldRule(List.of("input1", "input2"), List.of(), new Rule7("error message", true, false, true)), """
{
"inputs" : [ "input1", "input2" ],
"outputs" : [ ],
"rule7" : {
"message" : "error message",
"isError" : true
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of("output1"), new Rule7("info message", false, true, false)), """
{
"inputs" : [ ],
"outputs" : [ "output1" ],
"rule7" : {
"message" : "info message",
"isError" : false
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule8(0.5, 3, false, false)), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule8" : {
"probability" : 0.5,
"attempts" : 3
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of("output1"), new Rule8(1.0, 1, true, true)), """
{
"inputs" : [ "input1" ],
"outputs" : [ "output1" ],
"rule8" : {
"probability" : 1.0,
"attempts" : 1
}
}"""),
Arguments.of(new FieldRule(List.of("input1"), List.of("output1"), new Rule9("key1", "value1", true, false)), """
{
"inputs" : [ "input1" ],
"outputs" : [ "output1" ],
"rule9" : {
"key" : "key1",
"value" : "value1"
}
}"""),
Arguments.of(new FieldRule(List.of(), List.of(), new Rule9("key2", "", false, true)), """
{
"inputs" : [ ],
"outputs" : [ ],
"rule9" : {
"key" : "key2",
"value" : ""
}
}""")
);
}
@ParameterizedTest
@MethodSource("faked")
@DisplayName("test serialization and deserialization - generated")
void testSerializationAndDeserialization_UsingFaker(FieldRule input) throws JsonProcessingException {
var objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String json = objectMapper.writeValueAsString(input);
var deserialized = objectMapper.readValue(json, FieldRule.class);
System.out.println(json);
assertThat(deserialized)
.usingRecursiveComparison()
.isEqualTo(input);
}
private static Stream<FieldRule> faked() {
return Stream.generate(RuleSerializationTest::getRandomRule)
.limit(15)
.map(rule -> new FieldRule(generateRandomStringList(random.nextInt(3)), generateRandomStringList(random.nextInt(3)), rule));
}
private static Rule getRandomRule() {
int ruleIndex = random.nextInt(9) + 1;
return switch (ruleIndex) {
case 1 -> new Rule1(faker.lorem().sentence());
case 2 -> new Rule2(random.nextBoolean());
case 3 -> new Rule3(faker.number().randomDouble(2, 0, 100), random.nextInt(100));
case 4 -> new Rule4((char) (random.nextInt('z' - 'a') + 'a'), random.nextBoolean());
case 5 -> new Rule5(random.nextInt(100), faker.name().firstName());
case 6 -> new Rule6(faker.number().randomNumber(10), faker.number().randomDouble(2, 0, 100));
case 7 ->
new Rule7(faker.lorem().sentence(), random.nextBoolean(), random.nextBoolean(), random.nextBoolean());
case 8 ->
new Rule8(faker.number().randomDouble(2, 0, 1), random.nextInt(10), random.nextBoolean(), random.nextBoolean());
case 9 -> new Rule9(faker.lorem().word(), faker.lorem().word(), random.nextBoolean(), random.nextBoolean());
default -> throw new IllegalStateException("Unexpected value: " + ruleIndex);
};
}
private static List<String> generateRandomStringList(int size) {
return IntStream.range(0, size)
.mapToObj(i -> faker.lorem().word())
.toList();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment