Circe
GitHub概要
circe/circe
Yet another JSON library for Scala
スター2,521
ウォッチ61
フォーク544
作成日:2015年7月31日
言語:Scala
ライセンス:Apache License 2.0
トピックス
generic-derivationjsonscala
スター履歴
データ取得日時: 2025/10/22 09:50
ライブラリ
Circe
概要
Circeは「A JSON library for Scala powered by Cats」として開発された、純粋関数型プログラミングに基づくScala向けJSONライブラリです。CatsエコシステムとTypeClassesを活用し、JSONのエンコード・デコードにおける型安全性とパフォーマンスを両立。JSONデータの処理とバリデーションを同時に行う強力な機能を提供し、関数型プログラミングのパラダイムに忠実な設計により、コンパイル時の型安全性とランタイムの堅牢性を実現します。
詳細
Circe(発音:サーシー、ギリシャ神話の魔女キルケーから由来)は2025年現在、Scala 3対応の最新版0.14.xがリリースされており、Scala関数型プログラミングエコシステムにおける事実上の標準JSONライブラリです。TypeClassベースの設計により、カスタムデータ型に対する自動的なエンコーダー・デコーダー生成を提供。JSONスキーマバリデーション、カスタムバリデーションロジック、エラーハンドリングを統合的に扱い、コンパイル時の型安全性を保証します。14k+のGitHubスターを獲得し、Play Framework、Akka HTTP、ZIOなどのScalaエコシステムで広く採用されています。
主な特徴
- 型安全なJSONバリデーション: コンパイル時の型チェックによる安全なJSON処理
- 関数型エラーハンドリング: AccumulatingエラーによるFailureの詳細な収集
- 自動derivation: case classからのEncoder/Decoderの自動生成
- カスタムバリデーション: 柔軟なバリデーションルールの定義と組み合わせ
- 高いパフォーマンス: 効率的なJSON処理とメモリ使用量の最適化
- Catsエコシステム統合: Monad、Applicative、Validateなどの関数型抽象化
メリット・デメリット
メリット
- Scalaの型システムとシームレスな統合
- 関数型プログラミングによる予測可能で安全なコード
- 豊富なカスタマイズオプションと拡張性
- コンパイル時の最適化によるランタイムパフォーマンス
- エラーの詳細な情報と適切なエラーハンドリング
- Playframework、Akka、ZIOとの優れた統合
デメリット
- 関数型プログラミングの学習コストが高い
- TypeClassesやCatsの理解が必要
- 他のJVMライブラリとの相互運用性が限定的
- デバッグ時のスタックトレースが複雑になる場合がある
- JSONスキーマの動的生成が困難
- 初期設定とセットアップの複雑さ
参考ページ
書き方の例
SBTプロジェクトのセットアップ
// build.sbt
ThisBuild / scalaVersion := "3.3.1"
val circeVersion = "0.14.6"
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-refined" % circeVersion, // refinedTypes統合用
"io.circe" %% "circe-shapes" % circeVersion, // より高度なgeneric derivation
"io.circe" %% "circe-literal" % circeVersion % Test // リテラル補間用
)
// コンパイルオプション(Scala 3)
scalacOptions ++= Seq(
"-deprecation",
"-feature",
"-unchecked",
"-Wunused:all",
"-Yexplicit-nulls"
)
Maven設定
<properties>
<scala.version>3.3.1</scala.version>
<circe.version>0.14.6</circe.version>
</properties>
<dependencies>
<dependency>
<groupId>io.circe</groupId>
<artifactId>circe-core_3</artifactId>
<version>${circe.version}</version>
</dependency>
<dependency>
<groupId>io.circe</groupId>
<artifactId>circe-generic_3</artifactId>
<version>${circe.version}</version>
</dependency>
<dependency>
<groupId>io.circe</groupId>
<artifactId>circe-parser_3</artifactId>
<version>${circe.version}</version>
</dependency>
</dependencies>
基本的なJSONバリデーション
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import cats.syntax.either._
// ドメインモデルの定義
case class User(
id: Long,
name: String,
email: String,
age: Int
)
// 基本的なJSON解析とバリデーション
object BasicValidation {
def parseUser(jsonString: String): Either[Error, User] = {
decode[User](jsonString)
}
// 使用例
def main(args: Array[String]): Unit = {
val validJson = """
{
"id": 1,
"name": "田中太郎",
"email": "[email protected]",
"age": 30
}
"""
val invalidJson = """
{
"id": "invalid",
"name": "",
"email": "invalid-email",
"age": -1
}
"""
// 正常なケース
parseUser(validJson) match {
case Right(user) => println(s"バリデーション成功: $user")
case Left(error) => println(s"バリデーション失敗: $error")
}
// エラーケース
parseUser(invalidJson) match {
case Right(user) => println(s"バリデーション成功: $user")
case Left(error) => println(s"バリデーション失敗: $error")
}
}
}
カスタムバリデーションルール
import io.circe._
import io.circe.generic.semiauto._
import cats.data.Validated
import cats.syntax.validated._
import cats.syntax.apply._
// バリデーション済みのドメインオブジェクト
case class ValidatedUser(
id: UserId,
name: UserName,
email: Email,
age: Age
)
// 値オブジェクトによる型安全性
case class UserId(value: Long) extends AnyVal
case class UserName(value: String) extends AnyVal
case class Email(value: String) extends AnyVal
case class Age(value: Int) extends AnyVal
// カスタムバリデーション
object UserValidation {
type ValidationResult[A] = Validated[List[String], A]
// 個別フィールドのバリデーション
def validateUserId(id: Long): ValidationResult[UserId] = {
if (id > 0) UserId(id).valid
else List("ユーザーIDは正の値である必要があります").invalid
}
def validateUserName(name: String): ValidationResult[UserName] = {
if (name.nonEmpty && name.length <= 50) UserName(name).valid
else List("ユーザー名は1文字以上50文字以下である必要があります").invalid
}
def validateEmail(email: String): ValidationResult[Email] = {
val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$""".r
if (emailRegex.matches(email)) Email(email).valid
else List("有効なメールアドレス形式である必要があります").invalid
}
def validateAge(age: Int): ValidationResult[Age] = {
if (age >= 0 && age <= 150) Age(age).valid
else List("年齢は0以上150以下である必要があります").invalid
}
// 全体のバリデーション(Applicativeによるエラー蓄積)
def validateUser(
id: Long,
name: String,
email: String,
age: Int
): ValidationResult[ValidatedUser] = {
(
validateUserId(id),
validateUserName(name),
validateEmail(email),
validateAge(age)
).mapN(ValidatedUser.apply)
}
// JSONからのカスタムデコーダー
implicit val validatedUserDecoder: Decoder[ValidatedUser] = {
(c: HCursor) =>
for {
id <- c.downField("id").as[Long]
name <- c.downField("name").as[String]
email <- c.downField("email").as[String]
age <- c.downField("age").as[Int]
validated <- validateUser(id, name, email, age).toEither.leftMap { errors =>
DecodingFailure(s"バリデーションエラー: ${errors.mkString(", ")}", c.history)
}
} yield validated
}
}
複雑なJSONスキーマとネストされたバリデーション
import io.circe._
import io.circe.generic.auto._
import cats.data.{NonEmptyList, Validated}
import cats.syntax.validated._
import cats.syntax.traverse._
import cats.instances.list._
// 複雑なドメインモデル
case class Company(
id: Long,
name: String,
address: Address,
employees: List[Employee],
metadata: Map[String, String]
)
case class Address(
country: String,
prefecture: String,
city: String,
streetAddress: String,
postalCode: String
)
case class Employee(
id: Long,
name: String,
email: String,
department: Department,
salary: Option[BigDecimal],
joinDate: java.time.LocalDate
)
case class Department(
id: Long,
name: String,
budget: BigDecimal
)
object ComplexValidation {
type ValidationResult[A] = Validated[NonEmptyList[String], A]
// 住所のバリデーション
def validateAddress(address: Address): ValidationResult[Address] = {
val countryValidation = if (address.country.nonEmpty) address.country.valid
else "国名は必須です".invalidNel
val prefectureValidation = if (address.prefecture.nonEmpty) address.prefecture.valid
else "都道府県は必須です".invalidNel
val cityValidation = if (address.city.nonEmpty) address.city.valid
else "市区町村は必須です".invalidNel
val streetValidation = if (address.streetAddress.nonEmpty) address.streetAddress.valid
else "住所は必須です".invalidNel
val postalCodeValidation = {
val postalRegex = """^\d{3}-\d{4}$""".r
if (postalRegex.matches(address.postalCode)) address.postalCode.valid
else "郵便番号はXXX-XXXX形式である必要があります".invalidNel
}
(countryValidation, prefectureValidation, cityValidation, streetValidation, postalCodeValidation)
.mapN((_, _, _, _, _) => address)
}
// 部署のバリデーション
def validateDepartment(dept: Department): ValidationResult[Department] = {
val idValidation = if (dept.id > 0) dept.id.valid
else "部署IDは正の値である必要があります".invalidNel
val nameValidation = if (dept.name.nonEmpty) dept.name.valid
else "部署名は必須です".invalidNel
val budgetValidation = if (dept.budget > 0) dept.budget.valid
else "予算は正の値である必要があります".invalidNel
(idValidation, nameValidation, budgetValidation)
.mapN((_, _, _) => dept)
}
// 従業員のバリデーション
def validateEmployee(emp: Employee): ValidationResult[Employee] = {
val idValidation = if (emp.id > 0) emp.id.valid
else "従業員IDは正の値である必要があります".invalidNel
val nameValidation = if (emp.name.nonEmpty && emp.name.length <= 100) emp.name.valid
else "従業員名は1文字以上100文字以下である必要があります".invalidNel
val emailValidation = {
val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$""".r
if (emailRegex.matches(emp.email)) emp.email.valid
else "有効なメールアドレス形式である必要があります".invalidNel
}
val departmentValidation = validateDepartment(emp.department)
val salaryValidation = emp.salary match {
case Some(salary) if salary > 0 => emp.salary.valid
case Some(_) => "給与は正の値である必要があります".invalidNel
case None => emp.salary.valid
}
val joinDateValidation = {
val now = java.time.LocalDate.now()
val tenYearsAgo = now.minusYears(10)
if (emp.joinDate.isAfter(tenYearsAgo) && emp.joinDate.isBefore(now.plusDays(1))) {
emp.joinDate.valid
} else {
"入社日は過去10年以内かつ未来日ではない必要があります".invalidNel
}
}
(idValidation, nameValidation, emailValidation, departmentValidation, salaryValidation, joinDateValidation)
.mapN((_, _, _, dept, _, _) => emp.copy(department = dept))
}
// 会社全体のバリデーション
def validateCompany(company: Company): ValidationResult[Company] = {
val idValidation = if (company.id > 0) company.id.valid
else "会社IDは正の値である必要があります".invalidNel
val nameValidation = if (company.name.nonEmpty) company.name.valid
else "会社名は必須です".invalidNel
val addressValidation = validateAddress(company.address)
val employeesValidation = company.employees.traverse(validateEmployee)
val metadataValidation = {
if (company.metadata.size <= 20) company.metadata.valid
else "メタデータは20項目以下である必要があります".invalidNel
}
(idValidation, nameValidation, addressValidation, employeesValidation, metadataValidation)
.mapN((_, _, addr, emps, meta) => company.copy(address = addr, employees = emps, metadata = meta))
}
// カスタムデコーダーの定義
implicit val companyDecoder: Decoder[Company] = deriveDecoder[Company].validate { company =>
validateCompany(company).toEither.leftMap { errors =>
NonEmptyList.one(DecodingFailure(s"バリデーションエラー: ${errors.toList.mkString(", ")}", List.empty))
}
}
}
エラーハンドリングとカスタムエラーメッセージ
import io.circe._
import io.circe.syntax._
import cats.data.{NonEmptyList, Validated}
import cats.syntax.either._
// カスタムエラー型
sealed trait ValidationError
case class FieldError(field: String, message: String) extends ValidationError
case class BusinessRuleError(rule: String, message: String) extends ValidationError
case class StructuralError(message: String) extends ValidationError
object ErrorHandling {
type ValidationResult[A] = Validated[NonEmptyList[ValidationError], A]
// カスタムエラーハンドラー
class ValidationErrorHandler {
def handleFieldError(field: String, message: String): ValidationError = {
FieldError(field, message)
}
def handleBusinessRule(rule: String, message: String): ValidationError = {
BusinessRuleError(rule, message)
}
def handleStructuralError(message: String): ValidationError = {
StructuralError(message)
}
// エラーの日本語メッセージ生成
def toJapaneseMessage(error: ValidationError): String = error match {
case FieldError(field, message) => s"フィールド '$field': $message"
case BusinessRuleError(rule, message) => s"ビジネスルール '$rule': $message"
case StructuralError(message) => s"構造エラー: $message"
}
// エラーレスポンスの生成
def createErrorResponse(errors: NonEmptyList[ValidationError]): Json = {
val errorMessages = errors.map(toJapaneseMessage).toList
Json.obj(
"status" -> "error".asJson,
"message" -> "バリデーションエラーが発生しました".asJson,
"errors" -> errorMessages.asJson,
"timestamp" -> java.time.Instant.now().toString.asJson
)
}
}
// 高度なユーザーバリデーション
case class AdvancedUser(
id: Long,
username: String,
email: String,
profile: UserProfile,
preferences: UserPreferences
)
case class UserProfile(
firstName: String,
lastName: String,
birthDate: java.time.LocalDate,
phoneNumber: Option[String]
)
case class UserPreferences(
language: String,
timezone: String,
notifications: NotificationSettings
)
case class NotificationSettings(
email: Boolean,
push: Boolean,
sms: Boolean
)
class AdvancedUserValidator(errorHandler: ValidationErrorHandler) {
def validateId(id: Long): ValidationResult[Long] = {
if (id > 0) Validated.valid(id)
else Validated.invalidNel(errorHandler.handleFieldError("id", "正の値である必要があります"))
}
def validateUsername(username: String): ValidationResult[String] = {
val usernameRegex = """^[a-zA-Z0-9_]{3,20}$""".r
if (usernameRegex.matches(username)) Validated.valid(username)
else Validated.invalidNel(errorHandler.handleFieldError("username", "3-20文字の英数字とアンダースコアのみ使用可能です"))
}
def validateEmail(email: String): ValidationResult[String] = {
val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$""".r
if (emailRegex.matches(email)) Validated.valid(email)
else Validated.invalidNel(errorHandler.handleFieldError("email", "有効なメールアドレス形式である必要があります"))
}
def validateProfile(profile: UserProfile): ValidationResult[UserProfile] = {
val firstNameValidation = if (profile.firstName.nonEmpty) Validated.valid(profile.firstName)
else Validated.invalidNel(errorHandler.handleFieldError("firstName", "名前は必須です"))
val lastNameValidation = if (profile.lastName.nonEmpty) Validated.valid(profile.lastName)
else Validated.invalidNel(errorHandler.handleFieldError("lastName", "姓は必須です"))
val birthDateValidation = {
val now = java.time.LocalDate.now()
val eighteenYearsAgo = now.minusYears(18)
val hundredYearsAgo = now.minusYears(100)
if (profile.birthDate.isBefore(eighteenYearsAgo) && profile.birthDate.isAfter(hundredYearsAgo)) {
Validated.valid(profile.birthDate)
} else {
Validated.invalidNel(errorHandler.handleBusinessRule("age_restriction", "18歳以上100歳以下である必要があります"))
}
}
val phoneValidation = profile.phoneNumber match {
case Some(phone) =>
val phoneRegex = """^0\d{1,4}-\d{1,4}-\d{4}$""".r
if (phoneRegex.matches(phone)) Validated.valid(profile.phoneNumber)
else Validated.invalidNel(errorHandler.handleFieldError("phoneNumber", "日本の電話番号形式である必要があります"))
case None => Validated.valid(profile.phoneNumber)
}
(firstNameValidation, lastNameValidation, birthDateValidation, phoneValidation)
.mapN((_, _, _, _) => profile)
}
def validatePreferences(prefs: UserPreferences): ValidationResult[UserPreferences] = {
val supportedLanguages = Set("ja", "en", "ko", "zh")
val supportedTimezones = Set("Asia/Tokyo", "UTC", "America/New_York", "Europe/London")
val languageValidation = if (supportedLanguages.contains(prefs.language)) {
Validated.valid(prefs.language)
} else {
Validated.invalidNel(errorHandler.handleFieldError("language", s"サポートされている言語: ${supportedLanguages.mkString(", ")}"))
}
val timezoneValidation = if (supportedTimezones.contains(prefs.timezone)) {
Validated.valid(prefs.timezone)
} else {
Validated.invalidNel(errorHandler.handleFieldError("timezone", s"サポートされているタイムゾーン: ${supportedTimezones.mkString(", ")}"))
}
val notificationValidation = {
if (prefs.notifications.email || prefs.notifications.push || prefs.notifications.sms) {
Validated.valid(prefs.notifications)
} else {
Validated.invalidNel(errorHandler.handleBusinessRule("notification_required", "少なくとも1つの通知方法を選択する必要があります"))
}
}
(languageValidation, timezoneValidation, notificationValidation)
.mapN((_, _, _) => prefs)
}
def validateAdvancedUser(user: AdvancedUser): ValidationResult[AdvancedUser] = {
(
validateId(user.id),
validateUsername(user.username),
validateEmail(user.email),
validateProfile(user.profile),
validatePreferences(user.preferences)
).mapN((_, _, _, _, _) => user)
}
}
// カスタムデコーダーでのエラーハンドリング
implicit val advancedUserDecoder: Decoder[AdvancedUser] = {
val errorHandler = new ValidationErrorHandler()
val validator = new AdvancedUserValidator(errorHandler)
Decoder.instance { cursor =>
for {
rawUser <- Decoder[AdvancedUser].apply(cursor)
validatedUser <- validator.validateAdvancedUser(rawUser).toEither.leftMap { errors =>
DecodingFailure(errorHandler.createErrorResponse(errors).spaces2, cursor.history)
}
} yield validatedUser
}
}
}
パフォーマンス最適化とメモリ効率
import io.circe._
import io.circe.generic.semiauto._
import io.circe.streaming._
import cats.effect.IO
import fs2.Stream
// 大容量データの効率的な処理
object PerformanceOptimization {
// 軽量なバリデーション専用データ型
case class LightweightUser(
id: Long,
email: String,
status: String
)
// 最適化されたデコーダー(不要なフィールドをスキップ)
implicit val lightweightUserDecoder: Decoder[LightweightUser] = {
Decoder.instance { cursor =>
for {
id <- cursor.downField("id").as[Long]
email <- cursor.downField("email").as[String]
status <- cursor.downField("status").as[String]
} yield LightweightUser(id, email, status)
}
}
// ストリーミング処理によるメモリ効率化
def processLargeJsonStream(jsonStream: Stream[IO, String]): Stream[IO, Either[Error, LightweightUser]] = {
jsonStream
.through(stringStreamParser)
.through(decoder[IO, LightweightUser])
}
// バッチバリデーション(並列処理)
def validateUsersBatch(users: List[LightweightUser]): IO[List[Either[String, LightweightUser]]] = {
import cats.effect.implicits._
users.parTraverse { user =>
IO.pure {
validateLightweightUser(user).toEither.leftMap(_.mkString(", "))
}
}
}
def validateLightweightUser(user: LightweightUser): cats.data.Validated[List[String], LightweightUser] = {
import cats.syntax.validated._
import cats.syntax.apply._
val idValidation = if (user.id > 0) user.id.valid else List("IDが無効です").invalid
val emailValidation = if (user.email.contains("@")) user.email.valid else List("メールアドレスが無効です").invalid
val statusValidation = if (Set("active", "inactive", "pending").contains(user.status)) {
user.status.valid
} else {
List("ステータスが無効です").invalid
}
(idValidation, emailValidation, statusValidation).mapN((_, _, _) => user)
}
// キャッシュ機能付きバリデーター
class CachedValidator {
private val cache = collection.mutable.Map[String, Either[Error, LightweightUser]]()
def validateWithCache(jsonString: String): Either[Error, LightweightUser] = {
cache.getOrElseUpdate(jsonString, {
decode[LightweightUser](jsonString)
})
}
def clearCache(): Unit = cache.clear()
def cacheSize: Int = cache.size
}
// プロファイリング用のメトリクス
case class ValidationMetrics(
totalValidations: Long,
successfulValidations: Long,
failedValidations: Long,
averageProcessingTime: Double
) {
def successRate: Double = if (totalValidations > 0) successfulValidations.toDouble / totalValidations else 0.0
def failureRate: Double = if (totalValidations > 0) failedValidations.toDouble / totalValidations else 0.0
}
class MetricsCollector {
private var metrics = ValidationMetrics(0, 0, 0, 0.0)
private var totalTime = 0L
def recordValidation[T](validation: => Either[Error, T]): Either[Error, T] = {
val startTime = System.nanoTime()
val result = validation
val endTime = System.nanoTime()
val processingTime = endTime - startTime
totalTime += processingTime
result match {
case Right(_) =>
metrics = metrics.copy(
totalValidations = metrics.totalValidations + 1,
successfulValidations = metrics.successfulValidations + 1,
averageProcessingTime = totalTime.toDouble / metrics.totalValidations
)
case Left(_) =>
metrics = metrics.copy(
totalValidations = metrics.totalValidations + 1,
failedValidations = metrics.failedValidations + 1,
averageProcessingTime = totalTime.toDouble / metrics.totalValidations
)
}
result
}
def getMetrics: ValidationMetrics = metrics
def resetMetrics(): Unit = {
metrics = ValidationMetrics(0, 0, 0, 0.0)
totalTime = 0L
}
}
}
フレームワーク統合(Play Framework、Akka HTTP等)
// Play Framework統合
import play.api.mvc._
import play.api.libs.json.{Json => PlayJson}
import io.circe.parser._
import cats.data.Validated
class UserController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
def createUser(): Action[AnyContent] = Action { implicit request =>
request.body.asText match {
case Some(jsonString) =>
decode[ValidatedUser](jsonString) match {
case Right(user) =>
// ユーザー作成処理
Ok(PlayJson.obj("status" -> "success", "user" -> user.asJson))
case Left(error) =>
BadRequest(PlayJson.obj("status" -> "error", "message" -> error.getMessage))
}
case None =>
BadRequest(PlayJson.obj("status" -> "error", "message" -> "JSONデータが必要です"))
}
}
def validateUserBatch(): Action[AnyContent] = Action { implicit request =>
request.body.asText match {
case Some(jsonString) =>
decode[List[User]](jsonString) match {
case Right(users) =>
val validationResults = users.map { user =>
UserValidation.validateUser(user.id, user.name, user.email, user.age) match {
case Validated.Valid(validUser) =>
PlayJson.obj("status" -> "valid", "user" -> validUser.asJson)
case Validated.Invalid(errors) =>
PlayJson.obj("status" -> "invalid", "errors" -> errors.toList)
}
}
Ok(PlayJson.obj("results" -> validationResults))
case Left(error) =>
BadRequest(PlayJson.obj("status" -> "error", "message" -> error.getMessage))
}
case None =>
BadRequest(PlayJson.obj("status" -> "error", "message" -> "JSONデータが必要です"))
}
}
}
// Akka HTTP統合
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Route
object UserRoutes {
def routes: Route = {
pathPrefix("api" / "users") {
concat(
post {
entity(as[String]) { jsonString =>
decode[ValidatedUser](jsonString) match {
case Right(user) =>
complete(StatusCodes.OK, HttpEntity(ContentTypes.`application/json`, user.asJson.spaces2))
case Left(error) =>
val errorJson = Json.obj("error" -> error.getMessage.asJson)
complete(StatusCodes.BadRequest, HttpEntity(ContentTypes.`application/json`, errorJson.spaces2))
}
}
},
get {
parameters("id".as[Long]) { id =>
// ユーザー取得処理(バリデーション含む)
if (id > 0) {
val userJson = Json.obj("id" -> id.asJson, "message" -> "ユーザーが見つかりました".asJson)
complete(StatusCodes.OK, HttpEntity(ContentTypes.`application/json`, userJson.spaces2))
} else {
val errorJson = Json.obj("error" -> "無効なユーザーIDです".asJson)
complete(StatusCodes.BadRequest, HttpEntity(ContentTypes.`application/json`, errorJson.spaces2))
}
}
}
)
}
}
}
// ZIO統合
import zio._
import zio.json._
object ZIOIntegration {
case class ZIOUser(id: Long, name: String, email: String)
implicit val zioUserEncoder: JsonEncoder[ZIOUser] = DeriveJsonEncoder.gen[ZIOUser]
implicit val zioUserDecoder: JsonDecoder[ZIOUser] = DeriveJsonDecoder.gen[ZIOUser]
def validateZIOUser(jsonString: String): ZIO[Any, String, ZIOUser] = {
for {
user <- ZIO.fromEither(jsonString.fromJson[ZIOUser].left.map(_.getMessage))
validated <- if (user.id > 0 && user.name.nonEmpty && user.email.contains("@")) {
ZIO.succeed(user)
} else {
ZIO.fail("バリデーションエラー: 無効なユーザーデータです")
}
} yield validated
}
def processUsersBatch(jsonStrings: List[String]): ZIO[Any, Nothing, List[Either[String, ZIOUser]]] = {
ZIO.foreachPar(jsonStrings) { jsonString =>
validateZIOUser(jsonString).either
}
}
}