Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"permissions": {
"deny": [
"Read(**/obj/**)",
"Read(**/bin/**)",
"Read(**/.fable/**)",
"Read(**/__pycache__/**)",
"Read(**/*.pyc)"
]
}
}
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"rollForward": false
},
"fable": {
"version": "5.0.0-alpha.17",
"version": "5.0.0-alpha.20",
"commands": [
"fable"
],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
6.x
8.x
9.x
10.x

- name: Install just
uses: extractions/setup-just@v2
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
6.x
8.x
9.x
10.x
Expand Down
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Fable.Python

F# to Python compiler extension for Fable.

## Project Structure

- `src/stdlib/` - Python standard library bindings (Builtins, Json, Os, etc.)
- `src/flask/` - Flask web framework bindings
- `src/fastapi/` - FastAPI web framework bindings
- `src/pydantic/` - Pydantic model bindings
- `test/` - Test files
- `examples/` - Example applications (flask, fastapi, django, timeflies)
- `build/` - Generated Python output (gitignored)

## Build Commands

```bash
just clean # Clean all build artifacts (build/, obj/, bin/, .fable/)
just build # Build the project
just test-python # Run Python tests
just restore # Restore .NET and paket dependencies
just example-flask # Build and run Flask example
just example-fastapi # Build and run FastAPI example
just dev-fastapi # Run FastAPI with hot-reload
```

## Build Output

Generated Python code goes to `build/` directories (gitignored):
- `build/` - Main library output
- `build/tests/` - Test output
- `examples/*/build/` - Example outputs

## Key Concepts

### Fable Type Serialization

F# types compile to non-native Python types:

- `int` → `Int32` (not Python's `int`)
- `int64` → `Int64`
- F# array → `FSharpArray` (not Python's `list`)
- `ResizeArray<T>` → Python `list`
- `nativeint` → Python `int`

Use `Fable.Python.Json.dumps` with `fableDefault` for JSON serialization of Fable types.
Use `ResizeArray<T>` for collections in web API responses.
Use Pydantic `BaseModel` for FastAPI request/response types (handles `Int32` correctly).

See `JSON.md` for detailed serialization documentation.

### Decorator Attributes

Route decorators use `Py.DecorateTemplate`:

```fsharp
[<Erase; Py.DecorateTemplate("""app.get("{0}")""")>]
type GetAttribute(path: string) = inherit Attribute()
```

Class attributes use `Py.ClassAttributesTemplate` for Pydantic-style classes.
186 changes: 186 additions & 0 deletions JSON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# JSON Serialization with Fable.Python

This document explains how to properly serialize F# types to JSON when using Fable.Python.

## The Problem

When Fable compiles F# to Python, certain types are not native Python types:

| F# Type | Fable Python Type | Native Python? |
| ---------------- | -------------------------- | -------------- |
| `int` | `Int32` | No |
| `int64` | `Int64` | No |
| `float32` | `Float32` | No |
| F# record | Class with `__slots__` | No |
| F# union | Class with `tag`, `fields` | No |
| F# array | `FSharpArray` | No |
| `ResizeArray<T>` | `list` | Yes |
| `nativeint` | `int` | Yes |
| `string` | `str` | Yes |

Python's standard `json.dumps()` and web framework serializers (Flask's `jsonify`, FastAPI's `jsonable_encoder`) don't know how to serialize these Fable-specific types.

### Why Can't Fable's Int32 Just Inherit from Python's int?

Fable.Python uses [PyO3](https://pyo3.rs/) for its runtime. Due to [PyO3 limitations](https://github.com/PyO3/pyo3/issues/991), it's not possible to create a Rust type that subclasses Python's immutable `int` type. This means `Int32` is a separate type that needs special handling during serialization.

## The Solution: fableDefault

The `Fable.Python.Json` module provides a `fableDefault` function that handles Fable types:

```fsharp
open Fable.Python.Json

// Use the convenience function (recommended)
let jsonStr = dumps myObject

// Or use json.dumps with fableDefault explicitly
let jsonStr = json.dumps(myObject, ``default`` = fableDefault)

// With indentation
let prettyJson = dumpsIndented myObject 2
```

### What fableDefault Handles

| Type | Serialization |
| ------------------------------------- | ------------------------------------------- |
| `Int8`, `Int16`, `Int32`, `Int64` | → Python `int` |
| `UInt8`, `UInt16`, `UInt32`, `UInt64` | → Python `int` |
| `Float32`, `Float64` | → Python `float` |
| F# Records (with `__slots__`) | → Python `dict` |
| F# Unions (with `tag`, `fields`) | → `["CaseName", ...fields]` or `"CaseName"` |

## Usage Examples

### Basic Serialization

```fsharp
open Fable.Python.Json

// Anonymous record with F# int (compiles to Int32)
let data = {| id = 42; name = "Alice" |}
let json = dumps data
// Output: {"id": 42, "name": "Alice"}

// F# record
type User = { Id: int; Name: string }
let user = { Id = 1; Name = "Bob" }
let json = dumps user
// Output: {"Id": 1, "Name": "Bob"}

// F# discriminated union
type Status = Active | Inactive | Pending of string
let status = Pending "review"
let json = dumps status
// Output: ["Pending", "review"]
```

### With Web Frameworks

#### Flask

Flask's `jsonify` does **not** handle Fable types. Use `dumps` from `Fable.Python.Json`:

```fsharp
open Fable.Python.Flask
open Fable.Python.Json

[<APIClass>]
type Routes() =
[<Get("/users/<int:user_id>")>]
static member get_user(user_id: int) : string =
// Use dumps for Fable type support
dumps {| id = user_id; name = "Alice" |}

[<Get("/simple")>]
static member simple() : obj =
// jsonify works ONLY with native Python types
jsonify {| message = "Hello"; count = 42n |} // 'n' suffix = native int
```

#### FastAPI

FastAPI's `jsonable_encoder` does **not** handle Fable types in anonymous records. You have two options:

**Option 1: Use Pydantic models** (recommended for FastAPI)

```fsharp
open Fable.Python.FastAPI
open Fable.Python.Pydantic

[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>]
type UserResponse(Id: int, Name: string) =
inherit BaseModel()
member val Id: int = Id with get, set
member val Name: string = Name with get, set

[<APIClass>]
type API() =
[<Get("/users/{user_id}")>]
static member get_user(user_id: int) : UserResponse =
UserResponse(Id = user_id, Name = "Alice") // Works! Pydantic handles Int32
```

**Option 2: Use nativeint for anonymous records**

```fsharp
[<Delete("/items/{item_id}")>]
static member delete_item(item_id: int) : obj =
{| status = "deleted"; id = nativeint item_id |} // Convert to native int
```

### Collections

Use `ResizeArray<T>` instead of F# arrays for web API responses:

```fsharp
// Good - ResizeArray compiles to Python list
let users = ResizeArray<User>()
users.Add(User(Id = 1, Name = "Alice"))
let json = dumps users

// Avoid - F# array compiles to FSharpArray
let users = [| User(Id = 1, Name = "Alice") |] // May not serialize correctly
```

## Quick Reference

| Scenario | Solution |
|----------|----------|
| JSON API with Fable types | Use `Fable.Python.Json.dumps` |
| Flask endpoint | Use `dumps` instead of `jsonify` |
| FastAPI endpoint | Use Pydantic models or `nativeint` |
| Int literals in anonymous records | Use `42n` suffix for native int |
| Collections in API responses | Use `ResizeArray<T>` |
| F# array needed | Convert with `ResizeArray(myArray)` |

## API Reference

```fsharp
module Fable.Python.Json

/// Default serializer for Fable types
val fableDefault: obj -> obj

/// Serialize to JSON with Fable type support
val dumps: obj -> string

/// Serialize to JSON with indentation
val dumpsIndented: obj -> int -> string

/// Serialize to file with Fable type support
val dump: obj -> TextIOWrapper -> unit

/// Serialize to file with indentation
val dumpIndented: obj -> TextIOWrapper -> int -> unit

/// Raw Python json module (use with fableDefault for Fable types)
val json: IExports
```

## Further Reading

- [PyO3 Issue #991](https://github.com/PyO3/pyo3/issues/991) - Why Int32 can't subclass Python's int
- [Python json module](https://docs.python.org/3/library/json.html) - Standard library documentation
- [Pydantic](https://docs.pydantic.dev/) - Data validation for Python (works with Fable's Int32)
Loading