Skip to content

Refactor JSON format for attribute errors in Elastic4Play#490

Open
cchantep wants to merge 5 commits into
TheHive-Project:masterfrom
cchantep:task/e4s-attrerr-jsonfmt
Open

Refactor JSON format for attribute errors in Elastic4Play#490
cchantep wants to merge 5 commits into
TheHive-Project:masterfrom
cchantep:task/e4s-attrerr-jsonfmt

Conversation

@cchantep

@cchantep cchantep commented Jun 19, 2026

Copy link
Copy Markdown

Macro derivation of OWrites for individual *AttributeError types

Current JsonFormat call macro materialization on each call for the individual attribute error type.

private val invalidFormatAttributeErrorWrites = Writes[InvalidFormatAttributeError] { ifae =>
    Json.writes[InvalidFormatAttributeError]/* <-- !1! */.writes(ifae) +
      ("type"    -> JsString("InvalidFormatAttributeError")) +
      ("message" -> JsString(ifae.toString))
  }

// !1! Macro materialization of OWrites[InvalidFormatAttributeError] is called each time invalidFormatAttributeErrorWrites.
// Same of `unknownAttributeErrorWrites`, `updateReadOnlyAttributeErrorWrites`, `missingAttributeErrorWrites`.

When the current attributeCheckingExceptionWrites is used it could also materialize multiple time the same OWrites[*AttributeError] for multiple value of the same kind in errors.

implicit val attributeCheckingExceptionWrites: OWrites[AttributeCheckingError] = OWrites[AttributeCheckingError] { ace =>
    Json.obj(
      "tableName" -> ace.tableName,
      "type"      -> "AttributeCheckingError",
      "errors" -> JsArray(ace.errors.map {
        case e: InvalidFormatAttributeError  => invalidFormatAttributeErrorWrites.writes(e)
        case e: UnknownAttributeError        => unknownAttributeErrorWrites.writes(e)
        case e: UpdateReadOnlyAttributeError => updateReadOnlyAttributeErrorWrites.writes(e)
        case e: MissingAttributeError        => missingAttributeErrorWrites.writes(e)
      })
    )
  }

This could be improved, as such OWrites instances are invariants (do not depend on call).

'Manual' OWrites for AttributeCheckingError

The current OWrites instance for type AttributeCheckingError is 'manually' implemented (see bellow).
It could benefit from using the capability of macro derivation to supported sealed family (there AttributeError), so that if a new kind of error is added, it would be supported there without change.

Approach

A new test suit is introduced, with test(s) covering the existing implementation before any change, to make sure there is no regression.

cchantep added 5 commits June 19, 2026 16:29
Introduce common function `attributeErrorWrites`:

- Enforce format consistency between the different error kinds (for the `type` discriminator and `message` fields).
- Avoid calling macro materialization (micro-optimization) on each writes call (`OWrite[..] { Json.writes[..]/* !! */.writes(..) ... }`).
…ibuteCheckingError (no implementation change on itself)
…eckingError:

- Remove 'manual' discriminator from individual writes
- Use Play JSON capability to automatically derive `OWrites` for sealed trait
- Make sure the macro is configured with custom discriminator (field & value)
Json.writes[MissingAttributeError].writes(mae) +
("type" -> JsString("MissingAttributeError")) +
("message" -> JsString(mae.toString))
private def attributeErrorWrites[E <: AttributeError](underlying: OWrites[E]): OWrites[E] = {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current invalidFormatAttributeErrorWrites/unknownAttributeErrorWrites/updateReadOnlyAttributeErrorWrites/missingAttributeErrorWrites are private, only for attributeCheckingExceptionWrites to use them, so change there (such as removing the type field) do not introduce regression, as long as there is no regression in attributeCheckingExceptionWrites.

private[elastic4play] implicit val missingAttributeErrorWrites: OWrites[MissingAttributeError] = attributeErrorWrites(Json.writes[MissingAttributeError])

implicit val attributeCheckingExceptionWrites: OWrites[AttributeCheckingError] = {
val pkgName = classOf[AttributeError].getPackage.getName + '.'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As runtime invariant, it allows to move the *AttributeError to a different without change there.

implicit val attributeCheckingExceptionWrites: OWrites[AttributeCheckingError] = {
val pkgName = classOf[AttributeError].getPackage.getName + '.'

implicit def errWrites: OWrites[AttributeError] = Json.configured(JsonConfiguration(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom discriminator configuration (Play JSON default is "_json": "full.qualified.type.ClassName", the current Elastic4Play is "type": "ClassName") to avoid regression.

Json.toJsObject(missingAttrErr) must_=== missingAttrErrJson
}

"support AttributeCheckingError" in {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No regression test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant