Skip to content
Back to skills

1205 Data Modeling 2fabc0ae

ASecurity

Patterns for Serde serialization, FFI boundaries, and type safety.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added May 31, 2026
businessrustgoexpressapi

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 31, 2026

npx -y skills add tools-only/X-Skills --skill 1205-data-modeling_2fabc0ae --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1205 Data Modeling 2fabc0ae?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1205 Data Modeling 2fabc0ae
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1205-data-modeling-2fabc0ae/badge)](https://www.skillsdirectory.com/skills/tools-only-1205-data-modeling-2fabc0ae)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Data Modeling

Patterns for Serde serialization, FFI boundaries, and type safety.

## Table of Contents

- [Strong Types Over Strings](#strong-types-over-strings)
- [Serde Patterns](#serde-patterns)
- [UniFFI Boundaries](#uniffi-boundaries)
- [Lookup by Primary Key](#lookup-by-primary-key)

---

## Strong Types Over Strings

Strings for status/effort/triage lead to case-mismatch bugs. Use enums:

```rust
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Status { Open, InProgress, Done, Dismissed }

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Priority { P0, P1, P2, P3 }
```

**Naming convention**: `rename_all = "snake_case"` serializes `InProgress` as `"in_progress"`. For JSON APIs expecting camelCase, use `rename_all = "camelCase"`.

---

## Serde Patterns

### Versioned Data

When adding fields to serialized structures, old data won't have them:

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockInfo {
    pub pid: u32,
    pub path: String,

    #[serde(default)]
    pub proc_started: Option<u64>,  // New field - absent in old data

    #[serde(default, alias = "started")]
    pub created: Option<u64>,       // Supports old field name
}
```

**Key patterns**:

| Attribute | Purpose | Read/Write |
|-----------|---------|------------|
| `#[serde(default)]` | Missing fields deserialize as `Default::default()` | Read |
| `#[serde(alias = "old")]` | Accept old field name | Read only |
| `#[serde(rename = "new")]` | Write as different name | Write |
| `#[serde(skip_serializing_if = "Option::is_none")]` | Omit `None` values | Write |

**Important**: `alias` is **read-only**. If writing data that old code will read, use `rename`:

```rust
// Old code expects "started", new code uses "created"
#[serde(default, rename = "started")]  // Writes as "started"
pub created: Option<u64>,

// Or be explicit about asymmetry
#[serde(default, alias = "started", rename = "created")]
pub created: Option<u64>,  // Reads both, writes as "created"
```

### Strict External Input

For data from untrusted sources, use `deny_unknown_fields` to catch typos:

```rust
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExternalConfig {
    pub api_key: String,
    pub endpoint: String,
    // Typo like "endpont" will error instead of silently using default
}
```

### Sentinel Values

If spec uses sentinel strings (e.g., `Related: None`) but your type is `Option<String>`:

```rust
fn parse_optional_field(raw: &str) -> Option<String> {
    let t = raw.trim();
    if t.is_empty() || t.eq_ignore_ascii_case("none") {
        None
    } else {
        Some(t.to_string())
    }
}
```

For serde integration, use a custom deserializer:

```rust
fn deserialize_optional_sentinel<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: Option<String> = Option::deserialize(deserializer)?;
    Ok(s.and_then(|v| {
        let t = v.trim();
        if t.is_empty() || t.eq_ignore_ascii_case("none") {
            None
        } else {
            Some(t.to_string())
        }
    }))
}
```

---

## UniFFI Boundaries

UniFFI works best with flat, stable types. Keep DTOs separate from domain models.

### Basic DTO Pattern

```rust
// Internal domain model (expressive)
pub struct Idea {
    pub id: Uuid,
    pub created_at: DateTime<Utc>,
    pub status: Status,
    pub priority: Option<Priority>,
}

// FFI DTO (flat, stable)
#[derive(Clone, Debug, uniffi::Record)]
pub struct IdeaDto {
    pub id: String,           // Uuid → String at boundary
    pub created_at_ms: i64,   // DateTime → epoch ms
    pub status: String,       // Or use uniffi::Enum
    pub priority: Option<String>,
}

impl From<Idea> for IdeaDto {
    fn from(idea: Idea) -> Self {
        Self {
            id: idea.id.to_string(),
            created_at_ms: idea.created_at.timestamp_millis(),
            status: format!("{:?}", idea.status).to_lowercase(),
            priority: idea.priority.map(|p| format!("{:?}", p).to_lowercase()),
        }
    }
}
```

### Enums vs Strings

**Use `uniffi::Enum`** for closed sets where type safety matters:

```rust
#[derive(Clone, Debug, uniffi::Enum)]
pub enum StatusDto {
    Open,
    InProgress,
    Done,
    Dismissed,
    #[uniffi(default)]
    Unknown { value: String },  // Forward-compatible
}
```

The `#[uniffi(default)]` variant handles unknown values gracefully—foreign code receives `Unknown` instead of crashing on new variants.

**Use strings** when:
- The set is truly open-ended
- You can't regenerate bindings when adding variants
- Stability trumps type safety

### What to Avoid in FFI

| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `Uuid` | Not FFI-friendly | `String` |
| `DateTime<Utc>` | Complex type | `i64` (epoch ms) |
| Enums with data | Limited support | Flat enums or separate fields |
| Nested structs | Harder to maintain | Flatten or separate calls |
| `HashMap` | Limited in some targets | `Vec<(K, V)>` |

---

## Lookup by Primary Key

Use the resolved record's **primary key** for subsequent lookups, not secondary attributes:

```rust
// BAD: Secondary lookup could return different record
let resolved = resolve_state(&store, project_path)?;
let record = store.find_by_cwd(&resolved.cwd);  // Could match Session B!

// GOOD: Use the resolved session_id
let resolved = resolve_state(&store, project_path)?;
let record = resolved.session_id
    .as_deref()
    .and_then(|id| store.get_by_session_id(id));
```

**Why this matters**: Secondary attributes (like `cwd`) can match multiple records. After resolving to a specific record, always use its unique identifier for subsequent operations to avoid TOCTOU (time-of-check-to-time-of-use) bugs.

Files in this skill

  • README.md859 B
  • skill.md5.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.