8

I can't get Spring Data Rest with class inheritance working.

I'd like to have a single JSON Endpoint which handles all my concrete classes.

Repo:

public interface AbstractFooRepo extends KeyValueRepository<AbstractFoo, String> {}

Abstract class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
 @JsonSubTypes.Type(value = MyFoo.class, name = "MY_FOO")
})
public abstract class AbstractFoo {
 @Id public String id;
 public String type;
}

Concrete class:

public class MyFoo extends AbstractFoo { }

Now when calling POST /abstractFoos with {"type":"MY_FOO"}, it tells me: java.lang.IllegalArgumentException: PersistentEntity must not be null!.

This seems to happen, because Spring doesn't know about MyFoo.

Is there some way to tell Spring Data REST about MyFoo without creating a Repository and a REST Endpoint for it?

(I'm using Spring Boot 1.5.1 and Spring Data REST 2.6.0)

EDIT:

Application.java:

@SpringBootApplication
@EnableMapRepositories
public class Application {
 public static void main(String[] args) {
 SpringApplication.run(DemoApplication.class, args);
 }
}
asked Feb 8, 2017 at 19:01
3
  • Did you read this: stackoverflow.com/a/25241995/1201725? Commented Feb 12, 2017 at 19:55
  • Please show your complete classes. Commented Feb 13, 2017 at 7:16
  • These are my complete classes. Only class left is my default Application class. I'll append it, but it's kinda useless. Commented Feb 13, 2017 at 7:38

2 Answers 2

9

I'm using Spring Boot 1.5.1 and Spring Data Release Ingalls.

KeyValueRepository doesn't work with inheritance. It uses the class name of every saved object to find the corresponding key-value-store. E.g. save(new Foo()) will place the saved object within the Foo collection. And abstractFoosRepo.findAll() will look within the AbstractFoo collection and won't find any Foo object.

Here's the working code using MongoRepository:

Application.java

Default Spring Boot Application Starter.

@SpringBootApplication
public class Application {
 public static void main(String[] args) {
 SpringApplication.run(Application.class, args);
 }
}

AbstractFoo.java

  • I've tested include = JsonTypeInfo.As.EXISTING_PROPERTY and include = JsonTypeInfo.As.PROPERTY. Both seem to work fine!

  • It's even possible to register the Jackson SubTypes with a custom JacksonModule.

  • IMPORTANT: @RestResource(path="abstractFoos") is highly recommended. Else the _links.self links will point to /foos and /bars instead of /abstractFoos.


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
@JsonSubTypes({
 @JsonSubTypes.Type(value = Foo.class, name = "MY_FOO"),
 @JsonSubTypes.Type(value = Bar.class, name = "MY_Bar")
})
@Document(collection="foo_collection")
@RestResource(path="abstractFoos")
public abstract class AbstractFoo {
 @Id public String id;
 public abstract String getType();
}

AbstractFooRepo.java

Nothing special here

public interface AbstractFooRepo extends MongoRepository<AbstractFoo, String> { }

Foo.java & Bar.java

@Persistent
public class Foo extends AbstractFoo {
 @Override
 public String getType() {
 return "MY_FOO";
 }
}
@Persistent
public class Bar extends AbstractFoo {
 @Override
 public String getType() {
 return "MY_BAR";
 }
}

FooRelProvider.java

  • Without this part, the output of the objects would be separated in two arrays under _embedded.foos and _embedded.bars.
  • The supports method ensures that for all classes which extend AbstractFoo, the objects will be placed within _embedded.abstractFoos.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class FooRelProvider extends EvoInflectorRelProvider {
 @Override
 public String getCollectionResourceRelFor(final Class<?> type) {
 return super.getCollectionResourceRelFor(AbstractFoo.class);
 }
 @Override
 public String getItemResourceRelFor(final Class<?> type) {
 return super.getItemResourceRelFor(AbstractFoo.class);
 }
 @Override
 public boolean supports(final Class<?> delimiter) {
 return AbstractFoo.class.isAssignableFrom(delimiter);
 }
}

EDIT

  • Added @Persistent to Foo.java and Bar.java. (Adding it to AbstractFoo.java doesn't work). Without this annotation I got NullPointerExceptions when trying to use JSR 303 Validation Annotations within inherited classes.

Example code to reproduce the error:

public class A {
 @Id public String id;
 @Valid public B b;
 // @JsonTypeInfo + @JsonSubTypes
 public static abstract class B {
 @NotNull public String s;
 }
 // @Persistent <- Needed!
 public static class B1 extends B { }
}
answered Feb 14, 2017 at 22:43
Sign up to request clarification or add additional context in comments.

2 Comments

Heads up if using this for moving data in and out of mongodb with spring, you will need this as well: gist.github.com/letalvoj/978e6c975398693fc6843c5fe648416d
@Benjamin I saw ur JIRA issue about inheritance in SDR and there you mentioned you were able to use projection with inheritance hierarchy. I would be grateful if you could update it here as to how you got the projections to work
4
+250

Please see the discussion in this resolved jira task for details of what is currently supported in spring-data-rest regarding JsonTypeInfo. And this jira task on what is still missing.

To summarize - only @JsonTypeInfo with include=JsonTypeInfo.As.EXISTING_PROPERTY is working for serialization and deserialization currently.

Also, you need spring-data-rest 2.5.3 (Hopper SR3) or later to get this limited support.

Please see my sample application - https://github.com/mduesterhoeft/spring-data-rest-entity-inheritance/tree/fixed-hopper-sr3-snapshot

With include=JsonTypeInfo.As.EXISTING_PROPERTY the type information is extracted from a regular property. An example helps getting the point of this way of adding type information:

The abstract class:

@Entity @Inheritance(strategy= SINGLE_TABLE)
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
 include=JsonTypeInfo.As.EXISTING_PROPERTY,
 property="type")
@JsonSubTypes({
 @Type(name="DECIMAL", value=DecimalValue.class),
 @Type(name="STRING", value=StringValue.class)})
public abstract class Value {
 @Id @GeneratedValue(strategy = IDENTITY)
 @Getter
 private Long id;
 public abstract String getType();
}

And the subclass:

@Entity @DiscriminatorValue("D")
@Getter @Setter
public class DecimalValue extends Value {
 @Column(name = "DECIMAL_VALUE")
 private BigDecimal value;
 public String getType() {
 return "DECIMAL";
 }
}
answered Feb 14, 2017 at 19:06

5 Comments

This doesn't work. I'm using spring-data-rest 2.6.0. And use KeyValueRepository (no JPA). My AbstractFoo class has now include=JsonTypeInfo.As.EXISTING_PROPERTY and an abstract method public abstract String getType(). When posting data to /abstractFoos I still get PersistentEntity must not be null!. (Also tried Hopper SR7 with Spring Boot 1.4.4)
Do you have the full source code on github? Would like to have a closer look. Otherwise, it is hard to guess what's going wrong.
I just switched from KeyValueRepository to MongoRepository and now it's kinda working! Seems like KeyValueRepository can't handle inheritance. Still some issue: When executing GET /abstractFoos my Objects are located under _embedded.foos and _embedded.bars. Why aren't they just a single collection? Now I can't sort them. (Can be fixed: stackoverflow.com/questions/29021221/… )
I'll post an answer that behaves correctly for MongoRepository. I came across lot's of pitfalls... Thanks for your help. You'll get the reward :)
The simple class name can also be used as discrminator "as existing property": public String getType2() { return getClass().getSimpleName().toUpperCase();}

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.