Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tagged unions generated for Jackson @JsonTypeInfo with EXISTING_PROPERTY #582

Merged
merged 1 commit into from
Nov 29, 2020

Conversation

vojtechhabarta
Copy link
Owner

This change is expansion of tagged (discriminated) union feature introduced in #81. It adds support for Jackson polymorphic types discriminated by EXISTING_PROPERTY.

Already supported is PROPERTY variant of Jackson polymorphic types but there are issues with it. In this case Jackson adds discriminant property when serializing sub-class and uses this additional property to deserialize JSON object to correct Java sub-class. But this mechanism doesn't work in cases where type information is not available to Jackson. For example when passing List of such objects directly to Jackson like new ObjectMapper().writeValueAsString(List.of(new SomeSubClass())) then specified PROPERTY is not included. Another case where this mechanism doesn't work is Spring REST controller where return type of some method contains such class wrapped in generic class, for example Optional<T>, like this @GetMapping Optional<SomeRootClass> test() { return Optional.of(new SomeSubClass()); }.

Since using PROPERTY causes described issue it is now supported to use variant with EXISTING_PROPERTY. In this variant Jackson uses value of regular (existing) property when serializing those objects but it uses annotations when deserializing them. So it can be inconsistent. However it is possible to mitigate this disadvantage and get reasonable level of confidence. For example we can have final field filled in mandatory constructor call and use constants like this:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Square.class, name = Square.JSON_TYPE_NAME),
    @JsonSubTypes.Type(value = Rectangle.class, name = Rectangle.JSON_TYPE_NAME),
    @JsonSubTypes.Type(value = Circle.class, name = Circle.JSON_TYPE_NAME),
})
private abstract class Shape {

    public final String kind;

    public Shape(String kind) {
        this.kind = kind;
    }
}

public class Square extends Shape {
    public static final String JSON_TYPE_NAME = "square";

    public double size;

    public Square() {
        super(JSON_TYPE_NAME);
    }
}

public class Rectangle extends Shape {
    public static final String JSON_TYPE_NAME = "rectangle";

    public double width;
    public double height;

    public Rectangle() {
        super(JSON_TYPE_NAME);
    }
}

public class Circle extends Shape {
    public static final String JSON_TYPE_NAME = "circle";

    public double radius;

    public Circle() {
        super(JSON_TYPE_NAME);
    }
}

Or we could reuse values from annotations for example like this:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Square.class, name = "square"),
    @JsonSubTypes.Type(value = Rectangle.class, name = "rectangle"),
    @JsonSubTypes.Type(value = Circle.class, name = "circle"),
})
private abstract class Shape {
    public final String kind;

    public Shape() {
        this.kind = Utils.getJsonSubTypeName(Shape.class, this.getClass());
    }
}

public class Square extends Shape {
    public double size;
}

public class Rectangle extends Shape {
    public double width;
    public double height;
}

public class Circle extends Shape {
    public double radius;
}

public class Utils {
    public static String getJsonSubTypeName(Class<?> root, Class<?> subClass) {
        final JsonSubTypes jsonSubTypesAnnotation = root.getAnnotation(JsonSubTypes.class);
        if (jsonSubTypesAnnotation == null) {
            throw new RuntimeException(String.format(
                    "Class '%s' is not annotated with @JsonSubTypes annotation",
                    root.getName()));
        }
        for (JsonSubTypes.Type typeAnnotation : jsonSubTypesAnnotation.value()) {
            if (Objects.equal(typeAnnotation.value(), subClass)) {
                return typeAnnotation.name();
            }
        }
        throw new RuntimeException(String.format(
                "Class '%s' is not specified in @JsonSubTypes annotation of '%s' class",
                subClass.getName(), root.getName()));
    }
}

Both examples have, of course, their pros and cons.

@vojtechhabarta vojtechhabarta merged commit a52b4af into master Nov 29, 2020
@vojtechhabarta vojtechhabarta deleted the tagged-unions-existing-property branch November 29, 2020 09:42
@vojtechhabarta
Copy link
Owner Author

Released in v2.28.785.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant