-
-
Save ctrimble/a5d06376c1e716f1ca26 to your computer and use it in GitHub Desktop.
Implementing Json Schema oneOf using nested Jackson deserializers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"children": [ | |
{ | |
"name": "type a", | |
"description": "description a", | |
"a": "a value" | |
}, | |
{ | |
"name": "type b", | |
"description": "description b", | |
"b": "b value" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.ArrayList; | |
import java.util.List; | |
import org.junit.Test; | |
import com.fasterxml.jackson.core.JsonParser; | |
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.core.ObjectCodec; | |
import com.fasterxml.jackson.core.TreeNode; | |
import com.fasterxml.jackson.databind.DeserializationContext; | |
import com.fasterxml.jackson.databind.JsonDeserializer; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | |
import static org.hamcrest.Matchers.*; | |
import static org.hamcrest.MatcherAssert.*; | |
public class BasicOneOfTest { | |
public static interface Named { | |
public String getName(); | |
} | |
public static interface Described { | |
public String getDescription(); | |
} | |
public static class A implements Named, Described { | |
private String name; | |
private String description; | |
private String a; | |
public String getName() { return name; } | |
public void setName(String name) { this.name = name; } | |
public String getDescription() { return description; } | |
public void setDescription(String description) { this.description = description; } | |
public String getA() { return a; } | |
public void setA(String a) { this.a = a; } | |
} | |
public static class B implements Named, Described { | |
private String name; | |
private String description; | |
private String b; | |
public String getName() { return name; } | |
public void setName(String name) { this.name = name; } | |
public String getDescription() { return description; } | |
public void setDescription(String description) { this.description = description; } | |
public String getB() { return b; } | |
public void setB(String b) { this.b = b; } | |
} | |
public static class C { | |
// | |
// Deserializers like this would be generated for each property that contains a oneOf | |
// | |
public static class Children$Deserializer extends JsonDeserializer<Object> { | |
@Override | |
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { | |
// lots of details skipped here. | |
ObjectCodec codec = jp.getCodec(); | |
TreeNode tree = jp.readValueAsTree(); | |
if( !tree.isArray() ) throw new IOException("invalid input"); | |
List<Object> value = new ArrayList<Object>(); | |
for( int i = 0;; i++ ) { | |
TreeNode element = tree.get(i); | |
if( element == null ) break; | |
if( element.get("a") != null ) { | |
value.add(codec.readValue(element.traverse(), A.class)); | |
} | |
else if( element.get("b") != null ) { | |
value.add(codec.readValue(element.traverse(), B.class)); | |
} | |
else { | |
throw new IOException("invalid input"); | |
} | |
} | |
return value; | |
} | |
} | |
// | |
// Fields that contain oneOf types would use the internally generated deserializer. | |
// | |
@JsonDeserialize(using=Children$Deserializer.class) | |
public List<?> children; | |
// | |
// Interfaces common to all objects in a oneOf would be exposed on the getters and setters. | |
// | |
@SuppressWarnings("unchecked") | |
public <T extends Named & Described> List<T> getChildren() { return (List<T>) children; } | |
} | |
@SuppressWarnings("unchecked") | |
@Test | |
public void deserializeC() throws Exception { | |
ObjectMapper mapper = new ObjectMapper(); | |
try( InputStream in = this.getClass().getResourceAsStream("./basicOneOf.json")) { | |
C c = mapper.readValue(in, C.class); | |
// correct types | |
assertThat(c, hasProperty("children", contains( | |
allOf( | |
instanceOf(A.class), | |
hasProperty("name", equalTo("type a")), | |
hasProperty("description", equalTo("description a")), | |
hasProperty("a", equalTo("a value")) | |
), | |
allOf( | |
instanceOf(B.class), | |
hasProperty("name", equalTo("type b")), | |
hasProperty("description", equalTo("description b")), | |
hasProperty("b", equalTo("b value")) | |
) | |
))); | |
// access generically | |
assertThat(c.getChildren().get(0).getName(), equalTo("type a")); | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment