1

Please see update below on a quick way to reproduce exception

The following two classes form a circular reference:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, 
                  property = "code")
@Value
public class Department {
  String code;
  @ToString.Exclude
  List<Employee> employees;      
}

@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, 
                  property="id")
@Value
public class Employee {
  String name;
  Department department;
}

In a producer web service (using Jackson via Spring @RestController), these classes get serialized into JSON. Here's an example response returning a Department object:

{
  "code": "HR",
  "employees": [
    {
      "id": 1,
      "name": "Bob",
      "department": "HR"
    },
    {
      "id": 2,
      "name": "Sally",
      "department": "HR"
    }
  ]
}

In a consumer service (which is a @FeignClient of the producer), the data is attempted to be deserialized in the exact same two classes. However, I get the following exception:

Error while extracting response for type [class com.example.Department] and content type [application/json]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: No Object Id found for an instance of `com.example.Employee`, to assign to property 'id'; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: No Object Id found for an instance of `com.example.Employee`, to assign to property 'id' at [Source: (PushbackInputStream); line: 1, column: 65] (through reference chain: com.example.Department["employees"]->java.util.ArrayList[0])

feign.codec.DecodeException: Error while extracting response for type [class com.example.Department] and content type [application/json]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: No Object Id found for an instance of `com.example.Employee`, to assign to property 'id'; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: No Object Id found for an instance of `com.example.Employee`, to assign to property 'id'

at [Source: (PushbackInputStream); line: 1, column: 65] (through reference chain: com.example.Department["employees"]->java.util.ArrayList[0])

I'm using lombok.anyConstructor.addConstructorProperties=true to support constructor property setting ala this post. I get the same error if I directly use @ConstructorProperties so it's not a Lombok issue.

When I make the class mutable (e,g, replace lombok @Value with @Data) or don't have circular references, everything works fine. But I would like my classes to be immutable.

Is it possible to use @JsonIdentityInfo to support circular references and deserialize into immutable classes?

Update

This is easy to reproduce. Create a project with the two classes noted above.

Using this gradle build file:

plugins {
    id 'java-library'
}

repositories {
    jcenter()
}

dependencies {
    implementation 'com.fasterxml.jackson.core:jackson-core:2.12.0'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.12.0'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.12.0'
    testImplementation 'junit:junit:4.12'
}

Then, run this simple test:

public class DeserializationTest {

    @Test
    public void testDeserializationIntoImmutableClasses() throws JsonProcessingException {

        List<Employee> employees = new ArrayList<>();
        
        Department hr = new Department("HR", employees);
        
        Employee bob = new Employee("Bob", hr);
        Employee sally = new Employee("Sally", hr);
        
        employees.add(bob);
        employees.add(sally);

        ObjectMapper om = new ObjectMapper();
        //serialize object referenced by object variable hr
        String hrJson = om.writeValueAsString(hr);
        
        //test fails by throwing 
        // com.fasterxml.jackson.databind.exc.MismatchedInputException: 
        // No Object Id found for an instance of `com.example.Employee`, to 
        // assign to property 'id' at 
        // [Source: (String)"
        //    {"code":"HR","employees": 
        //      [{"id":1,"name":"Bob","department":"HR"}, 
        //       {"id":2,"name":"Sally","department":"HR"}]}"; 
        //  line: 1, column: 65] 
        // (through reference chain: com.example.Department["employees"]- 
        // >java.util.ArrayList[0])
        om.readValue(hrJson, Department.class);
        
    }

}

I'll look at adding to GitHub and include the link in a second update.

James
  • 2,876
  • 18
  • 72
  • 116
  • Although the specific error-message is bizarre, I think it makes sense that Jackson can't handle this. If you were to set about writing explicit Java code to instantiate these classes, you'd find that there's no sensible way to do so. (There are some hacks you could use -- for example, you could give your Department instance a *mutable* list of Employees, keep a reference to that list, and implicitly mutate the Department by adding to that list -- but that hack depends on your knowledge of the internals of Department's constructor. Jackson doesn't have that knowledge.) – ruakh Dec 21 '20 at 19:03
  • @ruakh - Thanks. Yeah, I noticed that while writing the unit test that I posted above. – James Dec 24 '20 at 16:57

0 Answers0