Skip to content

Commit

Permalink
Proper handling of Polymorphic types when JsonTypeInfo.Id is DEDUCTION (
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrayner committed Feb 13, 2022
1 parent e370f80 commit 4f1dcd9
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,7 @@ class JsonSchemaGenerator
}
}

case class PolymorphismInfo(typePropertyName:String, subTypeName:String)
case class PolymorphismInfo(typePropertyName:Option[String], subTypeName:String)

private def extractPolymorphismInfo(_type:JavaType):Option[PolymorphismInfo] = {
val maybeBaseType = ClassUtil.findSuperTypes(_type, null, false).asScala.find { cl =>
Expand All @@ -886,7 +886,7 @@ class JsonSchemaGenerator
case _ : MinimalClassNameIdResolver => extractMinimalClassnameId(baseType, _type)
case _ => idResolver.idFromValueAndType(null, _type.getRawClass)
}
PolymorphismInfo(serializer.getPropertyName, id)
PolymorphismInfo(Option(serializer.getPropertyName), id)

case x => throw new Exception(s"We do not support polymorphism using jsonTypeInfo.include() = $x")
}
Expand Down Expand Up @@ -1054,32 +1054,30 @@ class JsonSchemaGenerator
}

// Optionally add JsonSchemaInject to top-level
val renderProps:Boolean = selectAnnotation(ac, classOf[JsonSchemaInject]).map {
a =>
val merged = injectFromJsonSchemaInject(a, thisObjectNode)
merged == true // Continue to render props since we merged injection
}.getOrElse( true ) // nothing injected => of course we should render props
val renderProps:Boolean = selectAnnotation(ac, classOf[JsonSchemaInject]).forall {
a => injectFromJsonSchemaInject(a, thisObjectNode)
} // nothing injected => of course we should render props

if (renderProps) {

val propertiesNode = getOrCreateObjectChild(thisObjectNode, "properties")

extractPolymorphismInfo(_type).map {
case pi: PolymorphismInfo =>
extractPolymorphismInfo(_type).collect {
case PolymorphismInfo(Some(typePropertyName), subTypeName) =>
// This class is a child in a polymorphism config..
// Set the title = subTypeName
thisObjectNode.put("title", pi.subTypeName)
thisObjectNode.put("title", subTypeName)

// must inject the 'type'-param and value as enum with only one possible value
// This is done to make sure the json generated from the schema using this oneOf
// contains the correct "type info"
val enumValuesNode = JsonNodeFactory.instance.arrayNode()
enumValuesNode.add(pi.subTypeName)
enumValuesNode.add(subTypeName)

val enumObjectNode = getOrCreateObjectChild(propertiesNode, pi.typePropertyName)
val enumObjectNode = getOrCreateObjectChild(propertiesNode, typePropertyName)
enumObjectNode.put("type", "string")
enumObjectNode.set("enum", enumValuesNode)
enumObjectNode.put("default", pi.subTypeName)
enumObjectNode.put("default", subTypeName)

if (config.hidePolymorphismTypeProperty) {
// Make sure the editor hides this polymorphism-specific property
Expand All @@ -1088,19 +1086,18 @@ class JsonSchemaGenerator
optionsNode.put("hidden", true)
}

getRequiredArrayNode(thisObjectNode).add(pi.typePropertyName)
getRequiredArrayNode(thisObjectNode).add(typePropertyName)

if (config.useMultipleEditorSelectViaProperty) {
// https://github.com/jdorn/json-editor/issues/709
// Generate info to help generated editor to select correct oneOf-type
// when populating the gui/schema with existing data
val objectOptionsNode = getOrCreateObjectChild( thisObjectNode, "options")
val multipleEditorSelectViaPropertyNode = getOrCreateObjectChild( objectOptionsNode, "multiple_editor_select_via_property")
multipleEditorSelectViaPropertyNode.put("property", pi.typePropertyName)
multipleEditorSelectViaPropertyNode.put("value", pi.subTypeName)
multipleEditorSelectViaPropertyNode.put("property", typePropertyName)
multipleEditorSelectViaPropertyNode.put("value", subTypeName)
()
}

}

Some(new JsonObjectFormatVisitor with MySerializerProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.kjetland.jackson.jsonSchema
import java.time.{LocalDate, LocalDateTime, OffsetDateTime}
import java.util
import java.util.{Collections, Optional, TimeZone}

import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.{ArrayNode, MissingNode, ObjectNode}
import com.fasterxml.jackson.databind.{JavaType, JsonNode, ObjectMapper, SerializationFeature}
Expand All @@ -24,8 +23,10 @@ import com.kjetland.jackson.jsonSchema.testData.polymorphism3.{Child31, Child32,
import com.kjetland.jackson.jsonSchema.testData.polymorphism4.{Child41, Child42}
import com.kjetland.jackson.jsonSchema.testData.polymorphism5.{Child51, Child52, Parent5}
import com.kjetland.jackson.jsonSchema.testData.polymorphism6.{Child61, Parent6}
import com.kjetland.jackson.jsonSchema.testData.polymorphism7.{Child71, Child72, Parent7}
import com.kjetland.jackson.jsonSchema.testDataScala._
import com.kjetland.jackson.jsonSchema.testData_issue_24.EntityWrapper

import javax.validation.groups.Default
import org.scalatest.{FunSuite, Matchers}

Expand Down Expand Up @@ -520,6 +521,36 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
}
}

test("Generate schema for super class annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.DEDUCTION") {
// Java
{
val config = JsonSchemaConfig.vanillaJsonSchemaDraft4
val g = new JsonSchemaGenerator(_objectMapper, debug = true, config)

assertToFromJson(g, testData.child71, classOf[Parent7])
val jsonNode = assertToFromJson(g, testData.child71)
val schema = generateAndValidateSchema(g, classOf[Parent7], Some(jsonNode))

// we have two sub types
schema.at("/oneOf").asInstanceOf[ArrayNode]
.asScala
.toStream
.map(_.at("/$ref").asText()) should contain only ("#/definitions/Child71", "#/definitions/Child72") //

// child71 should include both parent and child properties
schema.at("/definitions/Child71/properties").asInstanceOf[ObjectNode]
.fieldNames()
.asScala
.toStream should contain only ("id", "firstName")

// child72 should include both parent and child properties
schema.at("/definitions/Child72/properties").asInstanceOf[ObjectNode]
.fieldNames()
.asScala
.toStream should contain only ("id", "middleName", "lastName")
}
}

test("Generate schema for interface annotated with @JsonTypeInfo - use = JsonTypeInfo.Id.MINIMAL_CLASS") {

// Java
Expand All @@ -537,7 +568,6 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
}
}


test("Generate schema for super class annotated with @JsonTypeInfo - include = JsonTypeInfo.As.EXISTING_PROPERTY") {

// Java
Expand Down Expand Up @@ -568,6 +598,8 @@ class JsonSchemaGeneratorTest extends FunSuite with Matchers {
}
}



test("Generate schema for class containing generics with same base type but different type arguments") {
{
val config = JsonSchemaConfig.vanillaJsonSchemaDraft4
Expand Down Expand Up @@ -1795,6 +1827,20 @@ trait TestData {
c
}

val child71 = {
val c = new Child71()
c.id = 1
c.firstName = "Jeff"
c
}
val child72 = {
val c = new Child72();
c.id = 2;
c.middleName = "Test"
c.lastName = "Tester"
c
}

val child2Scala = Child2Scala("pv", 12)
val child1Scala = Child1Scala("pv", "cs", "cs2", "cs3")
val pojoWithParentScala = PojoWithParentScala(pojoValue = true, child1Scala, "y", 13, booleanWithDefault = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kjetland.jackson.jsonSchema.testData.polymorphism7;

import java.util.Objects;

public class Child71 extends Parent7 {
public String firstName;

@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
final Child71 child71 = (Child71) o;
return Objects.equals(firstName, child71.firstName);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), firstName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.kjetland.jackson.jsonSchema.testData.polymorphism7;

import java.util.Objects;

public class Child72 extends Parent7 {
public String middleName;
public String lastName;

@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
final Child72 child72 = (Child72) o;
return Objects.equals(middleName, child72.middleName) && Objects.equals(lastName, child72.lastName);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), middleName, lastName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.kjetland.jackson.jsonSchema.testData.polymorphism7;

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = Child71.class)
@JsonSubTypes({@JsonSubTypes.Type(Child72.class), @JsonSubTypes.Type(Child71.class)})
abstract public class Parent7 {
@JsonProperty(required = true)
public Integer id;

@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Parent7 parent7 = (Parent7) o;
return Objects.equals(id, parent7.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

0 comments on commit 4f1dcd9

Please sign in to comment.