cbeust / klaxon

A JSON parser for Kotlin
Apache License 2.0
1.86k stars 121 forks source link

Unable to parse Boolean #341

Closed olk90 closed 3 years ago

olk90 commented 3 years ago

Hi there,

I am facing trouble when parsing a JSON file containing a Boolean with the following stacktrace:

com.beust.klaxon.KlaxonException: Unable to instantiate InventoryItem:
    Parameter available: expected kotlin.Boolean but received java.lang.Boolean (value: true)

    Parameter motRequired: expected kotlin.Boolean but received java.lang.Boolean (value: true)

    at com.beust.klaxon.JsonObjectConverter.initIntoUserClass(JsonObjectConverter.kt:115)
    at com.beust.klaxon.JsonObjectConverter.fromJson(JsonObjectConverter.kt:30)
    at com.beust.klaxon.DefaultConverter.fromJsonObject(DefaultConverter.kt:221)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:40)
    at com.beust.klaxon.DefaultConverter.fromCollection(DefaultConverter.kt:139)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:39)
    at com.beust.klaxon.JsonObjectConverter.retrieveKeyValues(JsonObjectConverter.kt:207)
    at com.beust.klaxon.JsonObjectConverter.initIntoUserClass(JsonObjectConverter.kt:66)
    at com.beust.klaxon.JsonObjectConverter.fromJson(JsonObjectConverter.kt:30)
    at com.beust.klaxon.DefaultConverter.fromJsonObject(DefaultConverter.kt:221)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:40)
    at com.beust.klaxon.Klaxon.fromJsonObject(Klaxon.kt:296)
    at de.olk90.inventorymanager.logic.controller.WorkspaceController.openDataContainer(WorkspaceController.kt:230)
    at de.olk90.inventorymanager.InventoryManagerApp.start(InventoryManagerApp.kt:30)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:474)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:447)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:446)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
    at java.base/java.lang.Thread.run(Thread.java:831)

Here's what the line at de.olk90.inventorymanager.logic.controller.WorkspaceController.openDataContainer(WorkspaceController.kt:230) looks like:

val dc = Klaxon().parse<DataContainer>(File(documentPath))

And here the DataContainer class:

data class DataContainer(
    val identifier: String = "",
    val persons: List<Person> = mutableListOf(),
    val items: List<InventoryItem> = mutableListOf(),
    val history: List<LendingHistoryRecord> = mutableListOf()
)

The class InventoryItem that seems to be the actual problem looks like this:

class InventoryItem(
    name: String = "",
    available: Boolean = false,
    lendingDate: LocalDate? = null,
    info: String = "",
    category: String = "",
    motRequired: Boolean = true,
    nextMot: LocalDate? = null,
) {

    @Json(ignored = true)
    val nameProperty = SimpleStringProperty(name)

    @Json(ignored = true)
    val availableProperty = SimpleBooleanProperty(available)

    @Json(ignored = true)
    val lendingDateProperty = SimpleObjectProperty(lendingDate)

    @Json(ignored = true)
    val infoProperty = SimpleStringProperty(info)

    @Json(ignored = true)
    val categoryProperty = SimpleStringProperty(category)

    @Json(ignored = true)
    val motRequiredProperty = SimpleBooleanProperty(motRequired)

    @Json(ignored = true)
    val nextMotProperty = SimpleObjectProperty(nextMot)

    val id = ObjectStore.nextInventoryId()

    var name: String
        get() = nameProperty.value
        set(value) = nameProperty.set(value)

    var available: Boolean
        get() = availableProperty.value
        set(value) = availableProperty.set(value)

    var lendingDate: LocalDate?
        get() = lendingDateProperty.value
        set(value) = lendingDateProperty.set(value)

    var info: String
        get() = infoProperty.value
        set(value) = infoProperty.set(value)

    var category: String
        get() = categoryProperty.value
        set(value) = categoryProperty.set(value)

    var motRequired: Boolean
        get() = motRequiredProperty.value
        set(value) = motRequiredProperty.set(value)

    var nextMot: LocalDate?
        get() = nextMotProperty.value
        set(value) = nextMotProperty.set(value)

}

It is class behind a JavaFX TableView, therefore the property fields. I'm currently working on a migration of the project from TornadoFX to JavaFX in order to be able to compile it for later JREs than Java 8. The original project is to be found here with the original class. This implementation is perfectly capable of parsing the related JSON file:

{
  "identifier": "Stuff",
  "persons": [{"email" : "gj@caesar.spqr", "firstName" : "Gaius J.", "id" : 0, "lastName" : "Caesar"}],
  "items": [{"available" : true, "category" : "None", "id" : 0, "info" : null, "lender" : -1, "lendingDate" : null, "motRequired" : true, "name" : "Sword", "nextMot" : null}],
  "history": [{"item" : 0, "lender" : 0, "lendingDate" : "2021-08-02", "returnDate" : "2021-08-01"}]
}

Does anyone have a clue, what's the problem here?

Edit

I've managed to get rid of the error by allowing the Boolean properties to be null (and added the missing fields from the JSON file as well):

class InventoryItem(
    name: String = "",
    available: Boolean? = false,
    lendingDate: LocalDate? = null,
    lender: Int? = -1,
    info: String = "",
    category: String = "",
    motRequired: Boolean? = true,
    nextMot: LocalDate? = null,
) {

    @Json(ignored = true)
    val nameProperty = SimpleStringProperty(name)

    @Json(ignored = true)
    val availableProperty = SimpleBooleanProperty(available?: false)

    @Json(ignored = true)
    val lendingDateProperty = SimpleObjectProperty(lendingDate)

    @Json(ignored = true)
    val lenderProperty = SimpleIntegerProperty(lender?: -1)

    @Json(ignored = true)
    val infoProperty = SimpleStringProperty(info)

    @Json(ignored = true)
    val categoryProperty = SimpleStringProperty(category)

    @Json(ignored = true)
    val motRequiredProperty = SimpleBooleanProperty(motRequired?: false)

    @Json(ignored = true)
    val nextMotProperty = SimpleObjectProperty(nextMot)

    val id = ObjectStore.nextInventoryId()

    var name: String
        get() = nameProperty.value
        set(value) = nameProperty.set(value)

    var available: Boolean?
        get() = availableProperty.value
        set(value) = availableProperty.set(value?: false)

    @Json(ignored = true)
    var lendingDate: LocalDate?
        get() = lendingDateProperty.value
        set(value) = lendingDateProperty.set(value)

    @Json(name = "lendingDate")
    var lendingDateString: String
        get() = lendingDateProperty.value.toString()
        set(value) {
            val formatter = DateTimeFormatter.ISO_LOCAL_DATE
            lendingDateProperty.set(LocalDate.parse(value, formatter))
        }

    var lender: Int
        get() = lenderProperty.value
        set(value) = lenderProperty.set(value)

    var info: String
        get() = infoProperty.value
        set(value) = infoProperty.set(value)

    var category: String
        get() = categoryProperty.value
        set(value) = categoryProperty.set(value)

    var motRequired: Boolean?
        get() = motRequiredProperty.value
        set(value) = motRequiredProperty.set(value?: false)

    @Json(ignored = true)
    var nextMot: LocalDate?
        get() = nextMotProperty.value
        set(value) = nextMotProperty.set(value)

    @Json(name = "nextMot")
    var nextMotString: String
        get() = nextMotProperty.value.toString()
        set(value) {
            val formatter = DateTimeFormatter.ISO_LOCAL_DATE
            nextMotProperty.set(LocalDate.parse(value, formatter))
        }

}

There is still a parsing error, but without any hint in the stacktrace:

com.beust.klaxon.KlaxonException: Unable to instantiate InventoryItem:

    at com.beust.klaxon.JsonObjectConverter.initIntoUserClass(JsonObjectConverter.kt:115)
    at com.beust.klaxon.JsonObjectConverter.fromJson(JsonObjectConverter.kt:30)
    at com.beust.klaxon.DefaultConverter.fromJsonObject(DefaultConverter.kt:221)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:40)
    at com.beust.klaxon.DefaultConverter.fromCollection(DefaultConverter.kt:139)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:39)
    at com.beust.klaxon.JsonObjectConverter.retrieveKeyValues(JsonObjectConverter.kt:207)
    at com.beust.klaxon.JsonObjectConverter.initIntoUserClass(JsonObjectConverter.kt:66)
    at com.beust.klaxon.JsonObjectConverter.fromJson(JsonObjectConverter.kt:30)
    at com.beust.klaxon.DefaultConverter.fromJsonObject(DefaultConverter.kt:221)
    at com.beust.klaxon.DefaultConverter.fromJson(DefaultConverter.kt:40)
    at com.beust.klaxon.Klaxon.fromJsonObject(Klaxon.kt:296)
    at de.olk90.inventorymanager.logic.controller.WorkspaceController.openDataContainer(WorkspaceController.kt:230)
    at de.olk90.inventorymanager.InventoryManagerApp.start(InventoryManagerApp.kt:30)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:474)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:447)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:446)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
    at java.base/java.lang.Thread.run(Thread.java:831)

I'll continue research and maybe try to fix the LocalDate mess first.

olk90 commented 3 years ago

Found the problem: The info field is null in the file and the InventoryItemdidn't allow info to be empty. If somebody steps into similar issues, here's the reworked class:

class InventoryItem (
    name: String = "",
    available: Boolean? = false,
    lendingDate: LendingDate = LendingDate(),
    lender: Int? = -1,
    info: String? = null,
    category: String = "",
    nextMot: MotDate = MotDate(),
    motRequired: Boolean? = true,
) {

    @Json(ignored = true)
    val nameProperty = SimpleStringProperty(name)

    @Json(ignored = true)
    val availableProperty = SimpleBooleanProperty(available ?: false)

    @Json(ignored = true)
    val lendingDateProperty = SimpleObjectProperty(lendingDate)

    @Json(ignored = true)
    val lenderProperty = SimpleIntegerProperty(lender ?: -1)

    @Json(ignored = true)
    val infoProperty = SimpleStringProperty(info)

    @Json(ignored = true)
    val categoryProperty = SimpleStringProperty(category)

    @Json(ignored = true)
    val motRequiredProperty = SimpleBooleanProperty(motRequired ?: false)

    @Json(ignored = true)
    val nextMotProperty = SimpleObjectProperty(nextMot)

    val id = ObjectStore.nextInventoryId()

    var name: String
        get() = nameProperty.value
        set(value) = nameProperty.set(value)

    var available: Boolean?
        get() = availableProperty.value
        set(value) = availableProperty.set(value ?: false)

    var lendingDate: LendingDate
        get() = lendingDateProperty.value
        set(value) = lendingDateProperty.set(value)

    var lender: Int
        get() = lenderProperty.value
        set(value) = lenderProperty.set(value)

    var info: String
        get() = infoProperty.value
        set(value) = infoProperty.set(value)

    var category: String
        get() = categoryProperty.value
        set(value) = categoryProperty.set(value)

    var motRequired: Boolean?
        get() = motRequiredProperty.value
        set(value) = motRequiredProperty.set(value ?: false)

    var nextMot: MotDate
        get() = nextMotProperty.value
        set(value) = nextMotProperty.set(value)

}

And here's the handling of the dates:

package de.olk90.inventorymanager.logic.datahelpers

import com.beust.klaxon.Converter
import com.beust.klaxon.JsonValue
import com.beust.klaxon.KlaxonException
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Target(AnnotationTarget.FIELD)
annotation class IsoDate

class LendingDate @JvmOverloads constructor(
    @IsoDate
    val date: LocalDate = LocalDate.of(2000, 1, 1)
)

val lendingDateConverter = object : Converter {

    override fun canConvert(cls: Class<*>) = cls == LocalDate::class.java

    override fun fromJson(jv: JsonValue) =
        if (jv.string != null) {
            val formatter = DateTimeFormatter.ISO_LOCAL_DATE
            LocalDate.parse(jv.string, formatter)
        } else {
            throw KlaxonException("Couldn't parse date: ${jv.string}")
        }

    override fun toJson(value: Any) = """ { "date" : $value } """

}

@Target(AnnotationTarget.FIELD)
annotation class MonthYearDate

class MotDate @JvmOverloads constructor(
    @MonthYearDate
    val date: LocalDate = LocalDate.of(2000, 1, 1)
)

val motDateConverter = object : Converter {

    override fun canConvert(cls: Class<*>) = cls == LocalDate::class.java

    override fun fromJson(jv: JsonValue): Any? {
        return if (jv.string != null) {
            val formatter = DateTimeFormatter.ofPattern("d/MMM/yyyy")
            LocalDate.parse(jv.string, formatter)
        } else {
            val formatter = DateTimeFormatter.ofPattern("d/MMM/yyyy")
            LocalDate.parse("1/Jan/2000", formatter)
        }
    }

    override fun toJson(value: Any) = """ { "date" : $value } """

}

With the dates, the file had to be andjusted, too:

{
  "identifier": "Stuff",
  "persons": [
    {
      "email": "gj@caesar.spqr",
      "firstName": "Gaius J.",
      "id": 0,
      "lastName": "Caesar"
    }
  ],
  "items": [
    {
      "available": true,
      "category": "None",
      "id": 0,
      "info": null,
      "lender": -1,
      "lendingDate": {
        "date": "2021-08-02"
      },
      "motRequired": true,
      "name": "Sword",
      "nextMot": {
        "date": "1/Jan/2024"
      }
    }
  ],
  "history": [
    {
      "item": 0,
      "lender": 0,
      "lendingDate": "2021-08-02",
      "returnDate": "2021-08-01"
    }
  ]
}