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

Proper handling of Polymorphic types when JsonTypeInfo.Id is DEDUCTION #165

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}