JRobi17X
JRobi17X

Reputation: 67

How to solve "Could not write JSON: Infinite recursion (StackOverflowError)" in Kotlin using Data Classes?

I know there are several posts in the topic, but I just couldn't find one where Kotlin Data Classes were used. So I'm trying to make a REST API with H2 Database in Kotlin, using Spring Boot, and I'm using Postman as well. Some of the attributes of my classes have List type. And everytime I try to add some value to these lists in Postman and then try to get the results, I get the following error:

enter image description here

I have three classes:

Recipe.kt:

@Entity
data class Recipe(
    @Id
    @SequenceGenerator(name = RECIPE_SEQUENCE, sequenceName = RECIPE_SEQUENCE, initialValue = 1, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    val name: String,
    var cookTime: String?,
    var servings: String?,
    var directions: String?,
    @OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
    @JsonManagedReference
    var ingredient: List<Ingredient>?,
    @ManyToMany
    @JsonManagedReference
    @JoinTable(
        name = "recipe_category",
        joinColumns = [JoinColumn(name = "recipe_id")],
        inverseJoinColumns = [JoinColumn(name = "category_id")]
    )
    var category: List<Category>?,
    @Enumerated(value = EnumType.STRING)
    var difficulty: Difficulty?
    ) { companion object { const val RECIPE_SEQUENCE: String = "RECIPE_SEQUENCE" } }

Category.kt

    @Entity
data class Category(
    @Id
    @SequenceGenerator(name = CATEGORY_SEQUENCE, sequenceName = CATEGORY_SEQUENCE, initialValue = 1, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    var name: String,
    @ManyToMany(mappedBy = "category")
    @JsonBackReference
    var recipe: List<Recipe>?
) { companion object { const val CATEGORY_SEQUENCE: String = "CATEGORY_SEQUENCE" } }

Ingredient.kt

    @Entity
data class Ingredient(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    var description: String?,
    var amount: BigDecimal?,
    @ManyToOne
    @JsonBackReference
    var recipe: Recipe?,
    var unitOfMeasure: String?
)

RecipeResponse.kt

data class RecipeResponse (var id:Long,
                           var name:String,
                           var cookTime:String?,
                           var servings:String?,
                           var directions:String?,
                           var ingredient:List<Ingredient>?,
                           var category: List<Category>?,
                           var difficulty: Difficulty?)

RecipeResource.kt

@RestController
@RequestMapping(value = [BASE_RECIPE_URL])
class RecipeResource(private val recipeManagementService: RecipeManagementService)
{

    @GetMapping
    fun findAll(): ResponseEntity<List<RecipeResponse>> = ResponseEntity.ok(this.recipeManagementService.findAll())

RecipeManagementService.kt

@Service
class RecipeManagementService (@Autowired private val recipeRepository: RecipeRepository,
                               private val addRecipeRequestTransformer: AddRecipeRequestTransformer) {

    fun findAll(): List<RecipeResponse> = this.recipeRepository.findAll().map(Recipe::toRecipeResponse)

Upvotes: 1

Views: 3398

Answers (2)

Adam N.
Adam N.

Reputation: 98

You can use @JsonIgnore as @Akash suggested but the result will not include category field in the response json. For a single recipe object response will look something like:

{"id":1,"name":"recipe"}

You can use @JsonManagedReference and @JsonBackReference instead. This way you will break the infinite loop but still have category list generated.

Your model will look something like:

data class Recipe(
    var id: Long = 0,
    val name: String,
    @JsonManagedReference
    var category: List<Category>,
)

data class Category(
    val id: Long = 0,
    var name: String,
    @JsonBackReference
    var recipe: List<Recipe>
)

and this will generate a json:

{"id":1,"name":"recipe","category":[{"id":1,"name":"cat"}]}

Upvotes: 1

Akash Jain
Akash Jain

Reputation: 336

You have to add @JsonIgnore on top of your @ManyToOne and @OneToMany fields. Spring will ignore those fields where it is returning that object. forex.

@Entity
data class Ingredient(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    var description: String?,
    var amount: BigDecimal?,
    @ManyToOne
    @JsonIgnore
    var recipe: Recipe?, // This field will not be returned in JSON response.
    var unitOfMeasure: String?
)

Here, also notice if you want to include some of the field of this ManyToOne or OneToMany relations in your response. You have to formulate your response using ObjectNode and then return it.

edit :


ObjectMapper objectMapper = new ObjectMapper();

@RestController
public ResponseEntity<String> someFunction() throws Exception {
    ObjectNode msg = objectMapper.createObjectNode();
    msg.put("success", true);
    msg.put("field1", object.getValue1());
    msg.put("field2", object.getValue2());
    return ResponseEntity.ok()
        .header("Content-Type", "application/json")
        .body(objectMapper.writerWithDefaultPrettyPrinter()
        .writeValueAsString(res));
}

The code here is in java you can convert this into Kotlin it will be same I think. Here, instead of object you can write your own object name ex. ingredient.getDescription() and can generate the response.

Upvotes: 1

Related Questions