{"id":37029897,"url":"https://github.com/47degrees/pbdirect","last_synced_at":"2026-01-14T03:37:09.524Z","repository":{"id":37103745,"uuid":"97582924","full_name":"47degrees/pbdirect","owner":"47degrees","description":"Read/Write Scala objects directly to Protobuf with no .proto file definitions","archived":false,"fork":true,"pushed_at":"2023-03-29T12:54:48.000Z","size":625,"stargazers_count":25,"open_issues_count":0,"forks_count":7,"subscribers_count":19,"default_branch":"main","last_synced_at":"2024-05-20T22:27:54.413Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"btlines/pbdirect","license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/47degrees.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null}},"created_at":"2017-07-18T09:58:35.000Z","updated_at":"2024-03-29T03:06:36.000Z","dependencies_parsed_at":"2023-02-12T00:01:06.003Z","dependency_job_id":null,"html_url":"https://github.com/47degrees/pbdirect","commit_stats":null,"previous_names":["47deg/pbdirect"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/47degrees/pbdirect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Fpbdirect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Fpbdirect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Fpbdirect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Fpbdirect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/47degrees","download_url":"https://codeload.github.com/47degrees/pbdirect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Fpbdirect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408850,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2026-01-14T03:37:08.846Z","updated_at":"2026-01-14T03:37:09.516Z","avatar_url":"https://github.com/47degrees.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n[![codecov.io](http://codecov.io/gh/47degrees/pbdirect/branch/master/graph/badge.svg)](http://codecov.io/gh/47degrees/pbdirect) [![Maven Central](https://img.shields.io/badge/maven%20central-0.5.1-green.svg)](https://oss.sonatype.org/#nexus-search;gav~com.47deg~pbdirect*) [![Latest version](https://img.shields.io/badge/pbdirect-0.5.1-green.svg)](https://index.scala-lang.org/47degrees/pbdirect) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://raw.githubusercontent.com/47degrees/pbdirect/master/LICENSE) [![Join the chat at https://gitter.im/47deg/pbdirect](https://badges.gitter.im/47deg/pbdirect.svg)](https://gitter.im/47deg/pbdirect?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge) [![GitHub Issues](https://img.shields.io/github/issues/47degrees/pbdirect.svg)](https://github.com/47degrees/pbdirect/issues)\n\n# PBDirect\n\nRead/Write Scala objects directly to Protobuf with no .proto file definitions\n\n## Context\n\nProtobuf is a fast and efficient way to serialize data. While .proto files are great to share schema definitions between components, it is sometimes much simpler and straightforward to directly encode Scala object without using a .proto schema definition file.\n\nPBDirect aims just that: Make it easier to serialize/deserialize into Protobuf.\n\n## Setup\n\nIn order to use PBDirect you need to add the following lines to your `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.47deg\" %% \"pbdirect\" % \"0.7.0\"\n```\n\n## Dependencies\n\nPBDirect depends on:\n - [protobuf-java](https://developers.google.com/protocol-buffers/docs/javatutorial) the Protobuf java library (maintained by Google)\n - [shapeless](https://github.com/milessabin/shapeless) for the generation of type-class instances\n - [cats](https://github.com/typelevel/cats) to deal with optional and repeated fields\n\n## Usage\n\nIn order to use PBDirect you need to import the following:\n\n```scala\nimport pbdirect._\n```\n\n## Example\n\n### Schema definition\n\nPBDirect serialises case classes into protobuf and there is no need for a .proto schema definition file.\n\n```scala\ncase class MyMessage(\n  @pbIndex(1) id: Option[Int],\n  @pbIndex(3) text: Option[String],\n  @pbIndex(5) numbers: List[Int]\n)\n```\n\nis equivalent to the following protobuf definition:\n\n```protobuf\nmessage MyMessage2 {\n  int32  id              = 1;\n  string text            = 3;\n  repeated int32 numbers = 5;\n}\n```\n\nNote that the `@pbIndex` annotation is optional. If it is not present, the field's position in the case class is used\nas its index. For example, an unannotated case class like:\n\n```scala\ncase class MyMessage2(\n  id: Option[Int],\n  text: Option[String],\n  numbers: List[Int]\n)\n```\n\nis equivalent to the following protobuf definition:\n\n```protobuf\nmessage MyMessage {\n  int32  id              = 1;\n  string text            = 2;\n  repeated int32 numbers = 3;\n}\n```\n\n### Serialization\n\nYou only need to call the `toPB` method on your case class. This method is implicitly added with `import pbdirect._`.\n\n```scala\nval message = MyMessage2(\n  id = Some(123),\n  text = Some(\"Hello\"),\n  numbers = List(1, 2, 3, 4)\n)\n// message: MyMessage2 = MyMessage2(\n//   id = Some(value = 123),\n//   text = Some(value = \"Hello\"),\n//   numbers = List(1, 2, 3, 4)\n// )\nval bytes = message.toPB\n// bytes: Array[Byte] = Array(\n//   8,\n//   123,\n//   18,\n//   5,\n//   72,\n//   101,\n//   108,\n//   108,\n//   111,\n//   26,\n//   4,\n//   1,\n//   2,\n//   3,\n//   4\n// )\n```\n\n### Deserialization\n\nDeserializing bytes into a case class is also straight forward. You only need to call the `pbTo[A]` method on the byte array containing the protobuf encoded data.\nThis method is added implicitly on all `Array[Byte]` by importing `pbdirect._`.\n\n```scala\nval bytes2: Array[Byte] = Array[Byte](8, 123, 26, 5, 72, 101, 108, 108, 111, 40, 1, 40, 2, 40, 3, 40, 4)\n// bytes2: Array[Byte] = Array(\n//   8,\n//   123,\n//   26,\n//   5,\n//   72,\n//   101,\n//   108,\n//   108,\n//   111,\n//   40,\n//   1,\n//   40,\n//   2,\n//   40,\n//   3,\n//   40,\n//   4\n// )\nval message2 = bytes2.pbTo[MyMessage2]\n// message2: MyMessage2 = MyMessage2(\n//   id = Some(value = 123),\n//   text = None,\n//   numbers = List(72, 101, 108, 108, 111)\n// )\n```\n\n## Extension\n\nYou might want to define your own formats for unsupported types.\nE.g. to add a format to write `java.time.Instant` you can do:\n\n```scala\nimport java.time.Instant\nimport cats.implicits._\n\nimplicit val instantFormat: PBFormat[Instant] =\n  PBFormat[Long].imap(Instant.ofEpochMilli(_))(_.toEpochMilli)\n// instantFormat: PBFormat[Instant] = pbdirect.PBFormat$$anon$1@5f808fd7\n```\n\nIf you only need a reader you can map over an existing `PBScalarValueReader`\n\n```scala\nimport java.time.Instant\n\nimplicit val instantReader: PBScalarValueReader[Instant] =\n  PBScalarValueReader[Long].map(Instant.ofEpochMilli(_))\n// instantReader: PBScalarValueReader[Instant] = pbdirect.PBScalarValueReaderImplicits$FunctorReader$$anon$2@72e7adf3\n```\n\nAnd for a writer you simply contramap over it:\n\n```scala\nimport java.time.Instant\n\nimplicit val instantWriter: PBScalarValueWriter[Instant] =\n  PBScalarValueWriter[Long].contramap(_.toEpochMilli)\n// instantWriter: PBScalarValueWriter[Instant] = pbdirect.PBScalarValueWriterImplicits$ContravariantWriter$$anon$2@2728d9dc\n```\n\n## Oneof fields\n\npbdirect supports protobuf [`oneof` fields](https://developers.google.com/protocol-buffers/docs/proto3#oneof) encoded as [Shapeless](https://github.com/milessabin/shapeless) Coproducts.\n\nFor example:\n\n```scala\nimport shapeless._\n\ncase class MyMessage3(\n  @pbIndex(1) number: Int,\n  @pbIndex(2,3,4) coproduct: Option[Int :+: String :+: Boolean :+: CNil]\n)\n```\n\nis equivalent to the following protobuf definition:\n\n```protobuf\nmessage MyMessage3 {\n  int32 number = 1;\n  oneof coproduct {\n    int32 a  = 2;\n    string b = 3;\n    bool c   = 4;\n  }\n}\n```\n\n`oneof` fields with exactly two branches can also be encoded using `Either`. For example:\n\n```scala\ncase class MyMessage4(\n  @pbIndex(1) number: Int,\n  @pbIndex(2,3) either: Option[Either[String, Boolean]]\n)\n```\n\nis equivalent to:\n\n```protobuf\nmessage MyMessage4 {\n  int32 number = 1;\n  oneof either {\n    string b = 2;\n    bool c   = 3;\n  }\n}\n```\n\nSupport for `oneof` fields comes with a couple of restrictions:\n\n* `oneof` fields must have a `@pbIndex` annotation containing the indices of each of the sub-fields\n* The type of `oneof` fields must be a Coproduct (or `Either`) wrapped in `Option[_]`. This is so that pbdirect can set the value to `None` when the field is missing when reading a message from protobuf.\n\n## Default values and missing fields\n\nWhen reading a protobuf message, pbdirect needs to handle missing fields by falling back to some default value. How it does this depends on the type of field.\n\nThe following table gives some examples of how pbdirect decodes missing fields:\n\n| Scala type | Value given to missing field |\n| --- | --- |\n| `Int`/`Short`/`Byte`/`Long` | `0` |\n| `Double`/`Float` | `0.0` |\n| `String` | `\"\"` |\n| `Array[Byte]` | empty array |\n| `List[_]` | empty list |\n| `Map[_, _]` | empty map |\n| Scala `Enumeration` or [enumeratum](https://github.com/lloydmeta/enumeratum) `IntEnum` | the entry with value 0 |\n| `Option[_]` | `None` |\n| `MyMessage` | an instance of `MyMessage` with all its fields set to their default values |\n| `Int :+: String :+: CNil` | (not supported) |\n| `Option[Int :+: String :+: CNil]` | `None` |\n\nIf you have defined your own `PBScalarValueReader` by mapping over one of the\nbuilt-in readers, you will get whatever value is produced by the default value\nof the underlying type.\n\nFor example, if your message looks like:\n\n```scala\ncase class MyMessage5(instant: Instant)\n```\n\nand you use the `instantReader` defined earlier, reading a message with the `instant` field missing would result in `1970-01-01T00:00:00Z`.\n\n## Packed repeated fields\n\nPrimitive repeated fields (ints, floats, doubles, enums and booleans) are\nencoded using the protobuf packed encoding by default.\n\nThis behaviour can be overriden using the `@pbUnpacked` annotation:\n\n```scala\ncase class UnpackedMessage(\n  @pbUnpacked() ints: List[Int]\n)\n```\n\n## Fancy integer types (signed/unsigned/fixed-width)\n\nYou can tell pbdirect that an Int/Long field should be encoded in a special way\nby tagging its type with `Signed`, `Unsigned` or `Fixed`. For example:\n\n```scala\nimport shapeless.tag.@@\nimport pbdirect.{Signed, Unsigned, Fixed}\n\ncase class IntsMessage(\n  normalInt            : Int,\n  signedInt            : Int @@ Signed,\n  unsignedInt          : Int @@ Unsigned,\n  fixedWidthInt        : Int @@ Fixed,\n  fixedSignedWidthInt  : Int @@ (Signed with Fixed),\n  normalLong           : Long,\n  signedLong           : Long @@ Signed,\n  unsignedLong         : Long @@ Unsigned,\n  fixedWidthLong       : Long @@ Fixed,\n  fixedSignedWidthLong : Long @@ (Signed with Fixed)\n)\n```\n\nwould correspond to the following Protobuf definition:\n\n\n```protobuf\nmessage IntsMessage {\n  int32      normalInt              =   1;\n  sint32     signedInt              =   2;\n  uint32     unsignedInt            =   3;\n  fixed32    fixedWidthInt          =   4;\n  sfixed32   fixedSignedWidthInt    =   5;\n  int64      normalLong             =   6;\n  sint64     signedLong             =   7;\n  uint64     unsignedLong           =   8;\n  fixed64    fixedWidthLong         =   9;\n  sfixed64   fixedSignedWidthLong   =   10;\n}\n```\n\nYou can also tag individual types inside coproducts, key and value types of maps\nand element types of lists:\n\n```scala\ncase class AnotherIntsMessage(\n  @pbIndex(1, 2) signedIntOrNormalInt  : (Int @@ Signed) :+: Int :+: CNil,\n  @pbIndex(3)    signedIntFixedLongMap : Map[Int @@ Signed, Long @@ Fixed],\n  @pbIndex(4)    signedIntList         : List[Int @@ Signed]\n)\n```\n\n# Copyright\n\npbdirect is designed and developed by 47 Degrees\n\nCopyright (C) 2019-2020 47 Degrees. \u003chttp://47deg.com\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F47degrees%2Fpbdirect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F47degrees%2Fpbdirect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F47degrees%2Fpbdirect/lists"}