{"id":18758014,"url":"https://github.com/alexitc/playsonify","last_synced_at":"2025-08-20T23:32:57.917Z","repository":{"id":29837977,"uuid":"122811569","full_name":"AlexITC/playsonify","owner":"AlexITC","description":"An opinionated micro-framework to help you build practical JSON APIs with Play Framework (or akka-http)","archived":false,"fork":false,"pushed_at":"2024-08-21T12:57:54.000Z","size":170,"stargazers_count":40,"open_issues_count":16,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-10T03:40:55.782Z","etag":null,"topics":["akka-http","functional-programming","hacktoberfest","json-api","library","mill","playframework","scala","scalactic","testing","webservices"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AlexITC.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-02-25T06:32:07.000Z","updated_at":"2023-11-15T23:29:36.000Z","dependencies_parsed_at":"2024-11-07T18:03:41.423Z","dependency_job_id":null,"html_url":"https://github.com/AlexITC/playsonify","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexITC%2Fplaysonify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexITC%2Fplaysonify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexITC%2Fplaysonify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexITC%2Fplaysonify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AlexITC","download_url":"https://codeload.github.com/AlexITC/playsonify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230471175,"owners_count":18231193,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["akka-http","functional-programming","hacktoberfest","json-api","library","mill","playframework","scala","scalactic","testing","webservices"],"created_at":"2024-11-07T17:45:11.055Z","updated_at":"2024-12-19T17:08:41.254Z","avatar_url":"https://github.com/AlexITC.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# playsonify\n\n[![Build Status](https://travis-ci.org/AlexITC/playsonify.svg?branch=master)](https://travis-ci.org/AlexITC/playsonify)\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e09923e277df4191ab2a1c3a4953ce41)](https://www.codacy.com/app/AlexITC/playsonify?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=AlexITC/playsonify\u0026amp;utm_campaign=Badge_Grade)\n[![Join the chat at https://gitter.im/playsonify/Lobby](https://badges.gitter.im/playsonify/Lobby.svg)](https://gitter.im/playsonify/Lobby?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n[![Maven Central](https://img.shields.io/maven-central/v/com.alexitc/playsonify_2.12.svg)](https://maven-badges.herokuapp.com/maven-central/com.alexitc/playsonify_2.12)\n\nAn opinionated micro-framework that helps you to build JSON APIs in a practical way, currently supporting Play Framework, also, there is experimental support for akka-http.\n\n\n\n**[Table of Contents](http://tableofcontent.eu)**\n\u003c!-- Table of contents generated generated by http://tableofcontent.eu --\u003e\n\n- [State](#state)\n- [Support](#support)\n- [Name](#name)\n- [Features](#features)\n- [What can playsonify do?](#what-can-playsonify-do)\n  - [Deserialization and serialization](#deserialization-and-serialization)\n  - [Simple authentication](#simple-authentication)\n  - [Automatic exception handling](#automatic-exception-handling)\n  - [Simple error accumulation](#simple-error-accumulation)\n  - [Controller testing is simple](#controller-testing-is-simple)\n- [Usage](#usage)\n  - [Add dependencies](#add-dependencies)\n  - [Familiarize with scalactic Or and Every](#familiarize-with-scalactic-or-and-every)\n  - [Familiarize with our type aliases](#familiarize-with-our-type-aliases)\n  - [Create your application specific errors](#create-your-application-specific-errors)\n  - [Define your authentication mechanism](#define-your-authentication-mechanism)\n  - [Define your JsonControllerComponents](#define-your-jsoncontrollercomponents)\n  - [Define your AbstractJsonController](#define-your-abstractjsoncontroller)\n  - [Create your controllers](#create-your-controllers)\n- [Development](#development)\n  - [Compile](#compile)\n  - [Test](#test)\n  - [Integrate with IntelliJ](#integrate-with-intellij)````\n\n\n\n## State\nThis library has been used for a year on the [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts) project.\n\n## Support\nThe library has been tested with the following versions, it might work with other versions that are not officially supported.\n- Scala 2.12\n- Play Framework 2.6\n- akka-http 10.1.5\n\nPlease notice that the documentation is specific to play-framework, while most concepts apply, you can look into the akka-http tests to see what's different:\n- [akka-http TestController](playsonify-akka-http/test/src/com/alexitc/playsonify/akka/controllers/TestController.scala)\n\nAdd these lines to your `build.sbt` file to get the integration with akka-http:\n```scala\nval playsonifyVersion = \"2.0.0\"\n\nlibraryDependencies ++= Seq(\n  \"com.alexitc\" %% \"playsonify-core\" % playsonifyVersion,\n  \"com.alexitc\" %% \"playsonify-akka-http\" % playsonifyVersion,\n  \"com.alexitc\" %% \"playsonify-sql\" % playsonifyVersion\n)\n```\n\n\n## Name\nThe name `playsonify` was inspired by mixing the `JSON.stringify` function from JavaScript and the Play Framework which is what it is built for (it might be worth considering another name now that akka-http is supported).\n\n\n## Features\n- Validate, deserialize and map the incoming request body to a model class automatically.\n- Serialize the result automatically.\n- Automatically map errors to the proper HTTP status code (OK, BAD_REQUEST, etc).\n- Support i18n easily.\n- Render several errors instead of just the first one.\n- Keeps error responses consistent.\n- Authenticate requests easily.\n- HTTP Idiomatic controller tests.\n- Primitives for offset-based pagination.\n- Primitives for sorting results.\n\n\n## What can playsonify do?\n\nTry it by yourself with this [simple-app](examples/simple-app).\n\nLet's define an input model:\n```scala\ncase class Person(name: String, age: Int)\n\nobject Person {\n  implicit val reads: Reads[Person] = Json.reads[Person]\n}\n```\n\nDefine an output model:\n```scala\ncase class HelloMessage(message: String)\n\nobject HelloMessage {\n  implicit val writes: Writes[HelloMessage] = Json.writes[HelloMessage]\n}\n```\n\nDefine a controller:\n```scala\nclass HelloWorldController @Inject() (components: MyJsonControllerComponents)\n    extends MyJsonController(components) {\n\n  import Context._\n\n  def hello = publicInput { context: HasModel[Person] =\u003e\n    val msg = s\"Hello ${context.model.name}, you are ${context.model.age} years old\"\n    val helloMessage = HelloMessage(msg)\n    val goodResult = Good(helloMessage)\n\n    Future.successful(goodResult)\n  }\n\n  def authenticatedHello = authenticated { context: Authenticated =\u003e\n    val msg = s\"Hello user with id ${context.auth}\"\n    val helloMessage = HelloMessage(msg)\n    val goodResult = Good(helloMessage)\n\n    Future.successful(goodResult)\n  }\n\n  def failedHello = public[HelloMessage] { context: Context =\u003e\n    val errors = Every(\n      UserError.UserEmailIncorrect,\n      UserError.UserAlreadyExist,\n      UserError.UserNotFound)\n\n    val badResult = Bad(errors)\n    Future.successful(badResult)\n  }\n\n  def exceptionHello = public[HelloMessage] { context: Context =\u003e\n    Future.failed(new RuntimeException(\"database unavailable\"))\n  }\n}\n```\n\nLast, define the routes file (conf/routes):\n```\nPOST /hello         controllers.HelloWorldController.hello()\nGET  /auth          controllers.HelloWorldController.authenticatedHello()\nGET  /errors        controllers.HelloWorldController.failedHello()\nGET  /exception     controllers.HelloWorldController.exceptionHello()\n```\n\nThese are some of the features that you get automatically:\n\n### Deserialization and serialization\nRequest:\n```bash\ncurl -H \"Content-Type: application/json\" \\\n  -X POST -d \\\n  '{\"name\":\"Alex\",\"age\":18}' \\\n   localhost:9000/hello\n```\n\nResponse:\n```\n{\"message\":\"Hello Alex, you are 18 years old\"}\n```\n\n\n### Simple authentication\nRequest:\n```bash\ncurl -H \"Authorization: 13\" localhost:9000/auth\n```\n\nResponse:\n```\n{\"message\":\"Hello user with id 13\"}\n```\n\n\n### Automatic exception handling\nRequest:\n```bash\ncurl -v localhost:9000/exception\n```\n\nResponse:\n```\n\u003c HTTP/1.1 500 Internal Server Error\n{\n    \"errors\": [\n        {\n            \"errorId\": \"ab5beaf9307a4e1ab90d242786a84b29\", \n            \"message\": \"Internal error\", \n            \"type\": \"server-error\"\n        }\n    ]\n}\n```\n\n### Simple error accumulation\nRequest:\n```bash\ncurl -v localhost:9000/errors\n```\n\nResponse:\n```\n\u003c HTTP/1.1 400 Bad Request\n{\n   \"errors\":[\n      {\n         \"type\":\"field-validation-error\",\n         \"field\":\"email\",\n         \"message\":\"The email format is incorrect\"\n      },\n      {\n         \"type\":\"field-validation-error\",\n         \"field\":\"email\",\n         \"message\":\"The user already exist\"\n      },\n      {\n         \"type\":\"field-validation-error\",\n         \"field\":\"userId\",\n         \"message\":\"The user was not found\"\n      }\n   ]\n}\n```\n\n\n### Controller testing is simple\n```scala\nclass HelloWorldControllerSpec extends MyPlayAPISpec {\n\n  override val application = guiceApplicationBuilder.build()\n\n  \"POST /hello\" should {\n    \"succeed\" in {\n      val name = \"Alex\"\n      val age = 18\n      val body =\n        s\"\"\"\n           |{\n           |  \"name\": \"$name\",\n           |  \"age\": $age\n           |}\n         \"\"\".stripMargin\n\n      val response = POST(\"/hello\", Some(body))\n      status(response) mustEqual OK\n\n      val json = contentAsJson(response)\n      (json \\ \"message\").as[String] mustEqual \"Hello Alex, you are 18 years old\"\n    }\n  }\n}\n```\n\n## Usage\nThe documentation assumes that you are already familiar with play-framework and it might be incomplete, you can always look into these applications:\n- The [example application](examples/simple-app).\n- The [Crypto Coin Alerts project](https://github.com/AlexITC/crypto-coin-alerts/tree/master/alerts-server/app/controllers).\n- The [XSN Block Explorer project](https://github.com/X9Developers/block-explorer/tree/master/server/app/controllers)\n\n### Add dependencies\nAdd these lines to your `build.sbt` file:\n```scala\nval playsonifyVersion = \"2.0.0\"\n\nlibraryDependencies ++= Seq(\n  \"com.alexitc\" %% \"playsonify-core\" % playsonifyVersion,\n  \"com.alexitc\" %% \"playsonify-play\" % playsonifyVersion,\n  \"com.alexitc\" %% \"playsonify-sql\" % playsonifyVersion,\n  \"com.alexitc\" %% \"playsonify-play-test\" % playsonifyVersion % Test // optional, useful for testing\n)\n```\n\n\n### Familiarize with scalactic Or and Every\nPlaysonify uses [scalactic Or and Every](http://www.scalactic.org/user_guide/OrAndEvery) a lot, in summary, we have replaced `Either[L, R]` with `Or[G, B]`, it allow us to construct value having a `Good` or a `Bad` result. Also, `Every` is a non-empty list which gives some compile time guarantees. \n\nAs you might have noted, the use of scalactic could be easily replaced with `scalaz` or `cats`, in the future, we might add support to let you choose which library to use.\n\n\n### Familiarize with our type aliases\nThere are some type aliases that are helpful to not be nesting a lot of types on the method signatures, see the [core package](playsonify-core/src/com/alexitc/playsonify/core/package.scala), it looks like this:\n\n```scala\ntype ApplicationErrors = Every[ApplicationError]\ntype ApplicationResult[+A] = A Or ApplicationErrors\ntype FutureApplicationResult[+A] = Future[ApplicationResult[A]]\ntype FuturePaginatedResult[+A] = FutureApplicationResult[PaginatedResult[A]]\n```\n\n- `ApplicationErrors` represents a non-empty list of errors.\n- `ApplicationResult` represents a result or a non-empty list of errors.\n- `FutureApplicationResult` represents a result or a non-empty list of error that will be available in the future (asynchronous result).\n\n### Create your application specific errors\nWe have already defined some top-level [application errors](playsonify-core/src/com/alexitc/playsonify/models/applicationErrors.scala), you are required to extend them in your error classes, this is crucial to get the correct mapping from an error to the HTTP status.\n```scala\ntrait InputValidationError extends ApplicationError\ntrait ConflictError extends ApplicationError\ntrait NotFoundError extends ApplicationError\ntrait AuthenticationError extends ApplicationError\ntrait ServerError extends ApplicationError {\n  // contains data private to the server\n  def cause: Option[Throwable]\n}\n```\n\nFor example, let's say that we want to define the possible errors related to a user, we could define some errors:\n```scala\n\nsealed trait UserError\n\nobject UserError {\n\n  case object UserAlreadyExist extends UserError with ConflictError {\n    override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {\n      val message = i18nService.render(\"user.error.alreadyExist\")\n      val error = FieldValidationError(\"email\", message)\n      List(error)\n    }\n  }\n\n  case object UserNotFound extends UserError with NotFoundError {\n    override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {\n      val message = i18nService.render(\"user.error.notFound\")\n      val error = FieldValidationError(\"userId\", message)\n      List(error)\n    }\n  }\n\n  case object UserEmailIncorrect extends UserError with InputValidationError {\n    override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {\n      val message = i18nService.render(\"user.error.incorrectEmail\")\n      val error = FieldValidationError(\"email\", message)\n      List(error)\n    }\n  }\n}\n```\n\nThen, when playsonify detects a `Bad` result, it will map the error to an HTTP status code in the following way:\n- InputValidationError -\u003e 404 (BAD_REQUEST).\n- ConflictError -\u003e 409 (CONFLICT).\n- NotFoundError -\u003e 404 (NOT_FOUND).\n- AuthenticationError -\u003e 401 (UNAUTHORIZED).\n- ServerError -\u003e 500 (INTERNAL_SERVER_ERROR).\n\nHence, your task is to tag your error types with these top-level errors properly and implement the `toPublicErrorList` to get an error that would be rendered to the user.\n\nHere you have a real example: [errors package](https://github.com/AlexITC/crypto-coin-alerts/tree/master/alerts-server/app/com/alexitc/coinalerts/errors).\n\nNotice that the you have the preferred user language to render errors in that language when possible.\n\n\n### Define your authentication mechanism\nYou are required to define your own [AbstractAuthenticatorService](playsonify-play/src/com/alexitc/playsonify/play/AbstractAuthenticatorService.scala), this service have the responsibility to decide which requests are authenticated and which ones are not, you first task is to define a model to represent an authenticated request, it is common to take the user or the user id for this, this model will be available in your controllers while dealing with authenticated requests.\n\nFor example, suppose that we'll use an `Int` to represent the id of the user performing the request, at first, define the errors that represents that a request wasn't authenticated, like this:\n\n```scala\nsealed trait SimpleAuthError\n\nobject SimpleAuthError {\n\n  case object InvalidAuthorizationHeader extends SimpleAuthError with AuthenticationError {\n\n    override def toPublicErrorList[L](i18nService: I18nService[L])(implicit lang: L): List[PublicError] = {\n      val message = i18nService.render(\"auth.error.invalidToken\")\n      val error = HeaderValidationError(\"Authorization\", message)\n      List(error)\n    }\n  }\n}\n\n```\n\nYou could have defined the errors without the `SimpleAuthError` trait, I prefer to define a parent trait just in case that I need to use the errors in another part of the application.\n\nThen, create your authenticator service, in this case, we'll a create a dummy authenticator which takes the value from the `Authorization` header and tries to convert it to an `Int` which we would be used as the user id (please, never use this unsecure approach):\n\n```scala\nclass DummyAuthenticatorService extends AbstractAuthenticatorService[Int] {\n\n  override def authenticate(request: Request[JsValue]): FutureApplicationResult[Int] = {\n    val userIdMaybe = request\n      .headers\n      .get(HeaderNames.AUTHORIZATION)\n      .flatMap { header =\u003e Try(header.toInt).toOption }\n\n    val result = Or.from(userIdMaybe, One(SimpleAuthError.InvalidAuthorizationHeader))\n    Future.successful(result)\n  }\n}\n```\n\nNote that you might want to use a specific error when the header is not present, also, while the `Future` is not required in this specific case, it allow us to implement different approaches, like calling an external web service in this step.\n\nHere you have a real example: [JWTAuthenticatorService](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/com/alexitc/coinalerts/services/JWTAuthenticatorService.scala).\n\n\n### Define your JsonControllerComponents\nIn order to provide your custom components, we'll create a custom [JsonControllerComponents](playsonify/src/com/alexitc/playsonify/JsonControllerComponents.scala), here you'll wire what you have just defined, for example:\n\n```scala\nclass MyJsonControllerComponents @Inject() (\n    override val messagesControllerComponents: MessagesControllerComponents,\n    override val executionContext: ExecutionContext,\n    override val publicErrorRenderer: PublicErrorRenderer,\n    override val i18nService: I18nPlayService,\n    override val authenticatorService: DummyAuthenticatorService)\n    extends JsonControllerComponents[Int]\n\n```\n\nHere you have a real example: [MyJsonControllerComponents](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/controllers/MyJsonControllerComponents.scala).\n\n\n\n### Define your AbstractJsonController\nLast, we need to define your customized [AbstractJsonController](playsonify-play/src/com/alexitc/playsonify/play/JsonControllerComponents.scala), using guice dependency injection could lead us to this example:\n\n```scala\nabstract class MyJsonController(components: MyJsonControllerComponents) extends AbstractJsonController(components) {\n\n  protected val logger = LoggerFactory.getLogger(this.getClass)\n\n  override protected def onServerError(error: ServerError): Unit = {\n    error.cause match {\n      case Some(cause) =\u003e\n        logger.error(s\"Unexpected internal error, id = ${error.id.string}, error = $error\", cause)\n\n      case None =\u003e\n        logger.error(s\"Unexpected internal error, id = ${error.id.string}, error = $error}\")\n    }\n  }\n}\n\n```\n\nHere you have a real example: [MyJsonController](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/controllers/MyJsonController.scala).\n\n\n### Create your controllers\nIt is time to create your own controllers, let's define an input model for the request body:\n```scala\ncase class Person(name: String, age: Int)\n\nobject Person {\n  implicit val reads: Reads[Person] = Json.reads[Person]\n}\n```\n\nNow, define the output response:\n```scala\ncase class HelloMessage(message: String)\n\nobject HelloMessage {\n  implicit val writes: Writes[HelloMessage] = Json.writes[HelloMessage]\n}\n```\n\nAnd the controller:\n```scala\nclass HelloWorldController @Inject() (components: MyJsonControllerComponents)\n    extends MyJsonController(components) {\n\n  import Context._\n\n  def hello = publicInput { context: HasModel[Person] =\u003e\n    val msg = s\"Hello ${context.model.name}, you are ${context.model.age} years old\"\n    val helloMessage = HelloMessage(msg)\n    val goodResult = Good(helloMessage)\n\n    Future.successful(goodResult)\n  }\n}\n```\n\nWhat about authenticating the request?\n```scala\n...\n  def authenticatedHello = authenticated { context: Authenticated =\u003e\n    val msg = s\"Hello user with id ${context.auth}\"\n    val helloMessage = HelloMessage(msg)\n    val goodResult = Good(helloMessage)\n\n    Future.successful(goodResult)\n  }\n...\n```\n\n\nHere you have a real example: [controllers package](https://github.com/AlexITC/crypto-coin-alerts/blob/master/alerts-server/app/controllers).\n\n\n## Development\nThe project is built using the [mill](https://github.com/lihaoyi/mill) build tool instead of `sbt`, hence, you need to install `mill` in order to build the project.\n\nThe project has been built using `mill 0.2.8`.\n\n### Compile\n`mill playsonify.compile`\n\n### Test\n`mill playsonify.test`\n\n### Integrate with IntelliJ\nThis step should be run everytime `build.sc` is modified:\n- `mill mill.scalalib.GenIdea/idea`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexitc%2Fplaysonify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexitc%2Fplaysonify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexitc%2Fplaysonify/lists"}