2

I'm running into an issue with a Spring Data REST application using the Spring Boot starter. I have a number of entities defined with various relationships. I'm able to retrieve them by key (generally), but I'm having problems with some collections of the same objects. I'm wondering if this is a JsonIdentityInfo issue or otherwise "breaking" the unique identifier stuff to prevent circular JSON generation.

There are ~500 entries in the Company table, and the page size is defaulting to 20. Going to the default endpoint (/company), it returns the error below. I can pull up individual Companies without issue (/company/1), including the same Company that it is trying to generate when the JSON exception is created.

When I step into the exception stack, I see that it is trying to generate JSON for the supportEmailAddress field. This is an row that may be referenced by multiple Company rows. Companies also have Contacts that have EmailAddresses in this table as well, but these are generally not shared between Companies or Contacts.

NOTE: I've looked at the SO question with a similar stack trace, but that question seemed revolve around a custom Serializer. I am not using a custom serializer.

A few things I've tried:

  • JsonIdentityInfo with and without scope attribute
  • JsonManagedReference and JsonBackReference
  • @Access(AccessType.PROPERTY) on @Id field

Library versions:

ext['hibernate.version'] = '5.1.0.Final'
ext['hibernateVersion'] = '5.1.0.Final'
ext['springVersion'] = '2.5.1.RELEASE'
ext['springBootVersion'] = '1.3.5.RELEASE'
ext['springDataCommonsVersion'] = '1.12.1.RELEASE'
ext['springDataJpaVersion'] = '1.10.1.RELEASE'
ext['springIntegrationVersion'] = '4.2.6.RELEASE'
ext['querydslVersion'] = '4.1.0'
ext['jacksonVersion'] = '2.8.0'
ext['jacksonJsr310Version'] = '2.8.0'

I've tried debugging through the serialization and the root issue is that the serializer gets confused when processing the company.supportEmailAddress.key. It is trying to output the key value but the serializer expects the key name to be next. The error occurs when the second reference to the same supportEmailAddress id is made.

Updated to Jackson 2.8.0. No change.

I added a simplified, handwritten example JSON at the end to show the structure I'm expecting. As you can see, the two Companies refer to the same supportEmailAddress object (same id). If I change to a different id, it renders correctly. I believe the second reference would actually just output the id instead of the remainder of the object, since it was already serialized once. It's my understanding this is a standard feature, and I've not done anything related to changing the Jackson default features.

Simplified Entities (accessors omitted):

Company:

@Entity
@Table(name = "T_Company")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "key", scope = Company.class)
public class Company extends AbstractCustomEntity<Long> {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "COMPANY_ID")
private Long key;

@Size(max = 50)
@Column(name = "NAME", nullable = false, length = 50, unique = true)
private String name;

@OneToMany(orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "company")
private Set<Alias> aliases;

@ManyToMany(mappedBy = "company")
private Set<Owner> owner;

@OneToMany(mappedBy = "agency", cascade = CascadeType.ALL)
private Set<Contact> contacts;

@ManyToOne
@JoinColumn(name = "SUPPORT_EMAIL_ADDRESS_ID")
private EmailAddress supportEmailAddress;

Owner:

@Entity
@Table(name = "T_OWNER")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "key", scope = Owner.class)
public class Owner extends AbstractCustomEntity<Long> {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "VENDOR_ID", nullable = false, updatable = false)
private Long key;

@NotNull
@Size( max = 50 )
@Column(name = "NAME",nullable = false, length = 50, unique = true)
private String name;

@ManyToMany( cascade = CascadeType.ALL)
@JoinTable(name = "T_OWNER_COMPANY"
, joinColumns = {@JoinColumn(name = "OWNER_ID")}
        , inverseJoinColumns = {@JoinColumn(name = "COMPANY_ID")}
)
private Set<Company> companies;

@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
private Set<Contact> contacts;

EmailAddress:

@Entity
@Table(name = "T_EMAIL_ADDR")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "key", scope = EmailAddress.class)
public class EmailAddress extends AbstractCustomEntity<Long> {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "EMAIL_ADDRESS_ID")
private Long key;

@Column(name = "EMAIL_ADDRESS_TYPE_NME")
@Enumerated(EnumType.STRING)
private EmailAddrType emailAddrType;

@Size(max = 200)
@Email
@Column(name = "EMAIL_ADDR", length = 200, nullable = false, updatable = false)
private String emailAddress;

Contact:

@Entity
@Table(name = "T_CONTACT")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "CATG", length = 6)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "key", scope = Contact.class)
public abstract class Contact extends AbstractCustomEntity<Long> {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "CONTACT_ID")
private Long key;

@Column(name = "CONTACT_NME", length = 100)
@Size(max = 100)
private String name;

@OneToOne
@JoinColumn(name = "EMAIL_ADDRESS_ID")
private EmailAddress emailAddress;

Stack trace:

org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Can not write a number, expecting field name; nested exception is com.fasterxml.jackson.core.JsonGenerationException: Can not write a number, expecting field name
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:276)
at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:100)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:222)
at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81)
at org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.handleReturnValue(ResourceProcessorHandlerMethodReturnValueHandler.java:113)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.AbstractRequestLoggingFilter.doFilterInternal(AbstractRequestLoggingFilter.java:220)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:522)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1095)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1502)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1458)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: com.fasterxml.jackson.core.JsonGenerationException: Can not write a number, expecting field name
at com.fasterxml.jackson.core.JsonGenerator._reportError(JsonGenerator.java:1676)
at com.fasterxml.jackson.core.json.UTF8JsonGenerator._verifyValueWrite(UTF8JsonGenerator.java:925)
at com.fasterxml.jackson.core.json.UTF8JsonGenerator.writeNumber(UTF8JsonGenerator.java:787)
at com.fasterxml.jackson.databind.ser.std.NumberSerializers$LongSerializer.serialize(NumberSerializers.java:188)
at com.fasterxml.jackson.databind.ser.impl.WritableObjectId.writeAsId(WritableObjectId.java:35)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase._serializeWithObjectId(BeanSerializerBase.java:584)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanSerializer.serialize(UnwrappingBeanSerializer.java:114)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter.serializeAsField(UnwrappingBeanPropertyWriter.java:127)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:678)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)
at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:985)
at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer.serialize(PersistentEntityJackson2Module.java:193)
at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer.serialize(PersistentEntityJackson2Module.java:140)
at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:985)
at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$NestedEntitySerializer.serialize(PersistentEntityJackson2Module.java:356)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:672)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:678)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase._serializeWithObjectId(BeanSerializerBase.java:600)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanSerializer.serialize(UnwrappingBeanSerializer.java:114)
at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter.serializeAsField(UnwrappingBeanPropertyWriter.java:127)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:678)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)
at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:985)
at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer.serialize(PersistentEntityJackson2Module.java:193)
at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer.serialize(PersistentEntityJackson2Module.java:140)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:616)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:519)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:31)
at org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer.serialize(Jackson2HalModule.java:340)
at org.springframework.hateoas.hal.Jackson2HalModule$HalResourcesSerializer.serialize(Jackson2HalModule.java:302)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:672)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:678)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:130)
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1428)
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:930)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:269)
... 57 more

Example JSON:

{
  "_embedded" : {
"companies" : [ {
  "key" : 1,
  "name" : "company1",
  "supportEmailAddress" : {
    "key" : 1,
    "emailAddrType" : "support",
    "emailAddress" : "email@support.com"
  }
  "aliases" : [ ],
  "contacts" : [ ],
  "owner" : {
    "key" : 1,
    "name" : "owner 1",
    "contacts" : [ ],
    "_links" : {
      "company" : {
        "href" : "http://localhost:8080/company/1"
      }
    }
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/company/1"
    },
    "company" : {
      "href" : "http://localhost:8080/company/1"
    },
    "aliases" : {
      "href" : "http://localhost:8080/company/1/aliases"
    },
    "contacts" : {
      "href" : "http://localhost:8080/company/1/contacts"
    }
  }
}, {
  "key" : 2,
  "name" : "company2",
  "supportEmailAddress" : {
    "key" : 1,
    "emailAddrType" : "support",
    "emailAddress" : "email@support.com"
  }
  "aliases" : [ ],
  "contacts" : [ ],
  "owner" : {
    "key" : 2,
    "name" : "owner 2",
    "contacts" : [ ],
    "_links" : {
      "company" : {
        "href" : "http://localhost:8080/company/2"
      }
    }
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/company/2"
    },
    "company" : {
      "href" : "http://localhost:8080/company/2"
    },
    "aliases" : {
      "href" : "http://localhost:8080/company/2/aliases"
    },
    "contacts" : {
      "href" : "http://localhost:8080/company/2/contacts"
    }
  }
} ]
  },
  "_links" : {
"self" : {
  "href" : "http://localhost:8080/company"
},
"profile" : {
  "href" : "http://localhost:8080/profile/company"
},
"search" : {
  "href" : "http://localhost:8080/company/search"
}
  },
  "page" : {
"size" : 20,
"totalElements" : 2,
"totalPages" : 1,
"number" : 0
  }
}

1 Answers1

0

I was getting the same error while having EAGER fetch specified on the nested entities (e.g. @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL) private Set<Contact> contacts; on your Owner entity).

As soon as there were more than 1 items in the nested collections, the serialization stopped working. I was able to work around the issue by specifying FetchType.LAZY on the nested collections, I guess that way the serializer doesn't get confused with the recursion?

I'd love to hear a proper explanation from someone more familiar with Jackson

Michal
  • 2,078
  • 23
  • 36