Skip to content

Latest commit

 

History

History
198 lines (155 loc) · 5.21 KB

File metadata and controls

198 lines (155 loc) · 5.21 KB
title Parse JSON Responses Safely
id http-json-responses
skillLevel beginner
applicationPatternId making-http-requests
summary Use Effect Schema to validate and parse HTTP JSON responses with type safety.
tags
http
json
schema
validation
getting-started
rule
description
Always validate HTTP responses with Schema to catch API changes at runtime.
author PaulJPhilp
related
http-hello-world
schema-hello-world
lessonOrder 1

Guideline

Use Effect Schema to validate HTTP JSON responses, ensuring the data matches your expected types at runtime.


Rationale

APIs can change without warning:

  1. Fields disappear - Backend removes a field
  2. Types change - String becomes number
  3. Nulls appear - Required field becomes optional
  4. New fields - Extra data you didn't expect

Schema validation catches these issues immediately.


Good Example

import { Effect, Console } from "effect"
import { Schema } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"
import { NodeHttpClient, NodeRuntime } from "@effect/platform-node"

// ============================================
// 1. Define response schemas
// ============================================

const PostSchema = Schema.Struct({
  id: Schema.Number,
  title: Schema.String,
  body: Schema.String,
  userId: Schema.Number,
})

type Post = Schema.Schema.Type<typeof PostSchema>

const PostArraySchema = Schema.Array(PostSchema)

// ============================================
// 2. Fetch and validate single item
// ============================================

const getPost = (id: number) =>
  Effect.gen(function* () {
    const client = yield* HttpClient.HttpClient

    const response = yield* client.get(
      `https://jsonplaceholder.typicode.com/posts/${id}`
    )
    const json = yield* HttpClientResponse.json(response)

    // Validate against schema - fails if data doesn't match
    const post = yield* Schema.decodeUnknown(PostSchema)(json)

    return post
  })

// ============================================
// 3. Fetch and validate array
// ============================================

const getPosts = Effect.gen(function* () {
  const client = yield* HttpClient.HttpClient

  const response = yield* client.get(
    "https://jsonplaceholder.typicode.com/posts"
  )
  const json = yield* HttpClientResponse.json(response)

  // Validate array of posts
  const posts = yield* Schema.decodeUnknown(PostArraySchema)(json)

  return posts
})

// ============================================
// 4. Handle validation errors
// ============================================

const safeGetPost = (id: number) =>
  getPost(id).pipe(
    Effect.catchTag("ParseError", (error) =>
      Effect.gen(function* () {
        yield* Console.error(`Invalid response format: ${error.message}`)
        // Return a default or fail differently
        return yield* Effect.fail(new Error(`Post ${id} has invalid format`))
      })
    )
  )

// ============================================
// 5. Schema with optional fields
// ============================================

const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
  phone: Schema.optional(Schema.String),        // May not exist
  website: Schema.optional(Schema.String),
  company: Schema.optional(
    Schema.Struct({
      name: Schema.String,
      catchPhrase: Schema.optional(Schema.String),
    })
  ),
})

const getUser = (id: number) =>
  Effect.gen(function* () {
    const client = yield* HttpClient.HttpClient

    const response = yield* client.get(
      `https://jsonplaceholder.typicode.com/users/${id}`
    )
    const json = yield* HttpClientResponse.json(response)

    return yield* Schema.decodeUnknown(UserSchema)(json)
  })

// ============================================
// 6. Run examples
// ============================================

const program = Effect.gen(function* () {
  yield* Console.log("=== Validated Single Post ===")
  const post = yield* getPost(1)
  yield* Console.log(`Title: ${post.title}`)

  yield* Console.log("\n=== Validated Posts Array ===")
  const posts = yield* getPosts
  yield* Console.log(`Fetched ${posts.length} posts`)

  yield* Console.log("\n=== User with Optional Fields ===")
  const user = yield* getUser(1)
  yield* Console.log(`User: ${user.name}`)
  yield* Console.log(`Company: ${user.company?.name ?? "N/A"}`)
})

program.pipe(
  Effect.provide(NodeHttpClient.layer),
  NodeRuntime.runMain
)

Schema Helpers

Schema Purpose
Schema.String String value
Schema.Number Number value
Schema.Boolean Boolean value
Schema.Struct({...}) Object with fields
Schema.Array(schema) Array of items
Schema.optional(schema) May be undefined
Schema.NullOr(schema) May be null

Error Handling

Effect.catchTag("ParseError", (error) => {
  // error.message contains validation details
  // error.actual contains the invalid data
})

Best Practices

  1. Define schemas upfront - Document expected API format
  2. Use optional for unreliable fields - APIs change
  3. Handle ParseError - Graceful degradation
  4. Log validation failures - Debug API issues