Skip to content

Commit 826629e

Browse files
authored
feat: Added bindings for Pydantic and FastAPI + examples (#151)
* Updated examples for v5 * build: Use fable-5.0.0-alpha.20 * build: Remove temporary mitigation
1 parent 082ddb2 commit 826629e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+5047
-2140
lines changed

.claude/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"permissions": {
3+
"deny": [
4+
"Read(**/obj/**)",
5+
"Read(**/bin/**)",
6+
"Read(**/.fable/**)",
7+
"Read(**/__pycache__/**)",
8+
"Read(**/*.pyc)"
9+
]
10+
}
11+
}

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"rollForward": false
1111
},
1212
"fable": {
13-
"version": "5.0.0-alpha.17",
13+
"version": "5.0.0-alpha.20",
1414
"commands": [
1515
"fable"
1616
],

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ jobs:
2020
uses: actions/setup-dotnet@v5
2121
with:
2222
dotnet-version: |
23-
6.x
2423
8.x
2524
9.x
25+
10.x
2626
2727
- name: Install just
2828
uses: extractions/setup-just@v2

.github/workflows/release-please.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ jobs:
3434
uses: actions/setup-dotnet@v5
3535
with:
3636
dotnet-version: |
37-
6.x
3837
8.x
3938
9.x
4039
10.x

CLAUDE.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Fable.Python
2+
3+
F# to Python compiler extension for Fable.
4+
5+
## Project Structure
6+
7+
- `src/stdlib/` - Python standard library bindings (Builtins, Json, Os, etc.)
8+
- `src/flask/` - Flask web framework bindings
9+
- `src/fastapi/` - FastAPI web framework bindings
10+
- `src/pydantic/` - Pydantic model bindings
11+
- `test/` - Test files
12+
- `examples/` - Example applications (flask, fastapi, django, timeflies)
13+
- `build/` - Generated Python output (gitignored)
14+
15+
## Build Commands
16+
17+
```bash
18+
just clean # Clean all build artifacts (build/, obj/, bin/, .fable/)
19+
just build # Build the project
20+
just test-python # Run Python tests
21+
just restore # Restore .NET and paket dependencies
22+
just example-flask # Build and run Flask example
23+
just example-fastapi # Build and run FastAPI example
24+
just dev-fastapi # Run FastAPI with hot-reload
25+
```
26+
27+
## Build Output
28+
29+
Generated Python code goes to `build/` directories (gitignored):
30+
- `build/` - Main library output
31+
- `build/tests/` - Test output
32+
- `examples/*/build/` - Example outputs
33+
34+
## Key Concepts
35+
36+
### Fable Type Serialization
37+
38+
F# types compile to non-native Python types:
39+
40+
- `int``Int32` (not Python's `int`)
41+
- `int64``Int64`
42+
- F# array → `FSharpArray` (not Python's `list`)
43+
- `ResizeArray<T>` → Python `list`
44+
- `nativeint` → Python `int`
45+
46+
Use `Fable.Python.Json.dumps` with `fableDefault` for JSON serialization of Fable types.
47+
Use `ResizeArray<T>` for collections in web API responses.
48+
Use Pydantic `BaseModel` for FastAPI request/response types (handles `Int32` correctly).
49+
50+
See `JSON.md` for detailed serialization documentation.
51+
52+
### Decorator Attributes
53+
54+
Route decorators use `Py.DecorateTemplate`:
55+
56+
```fsharp
57+
[<Erase; Py.DecorateTemplate("""app.get("{0}")""")>]
58+
type GetAttribute(path: string) = inherit Attribute()
59+
```
60+
61+
Class attributes use `Py.ClassAttributesTemplate` for Pydantic-style classes.

JSON.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# JSON Serialization with Fable.Python
2+
3+
This document explains how to properly serialize F# types to JSON when using Fable.Python.
4+
5+
## The Problem
6+
7+
When Fable compiles F# to Python, certain types are not native Python types:
8+
9+
| F# Type | Fable Python Type | Native Python? |
10+
| ---------------- | -------------------------- | -------------- |
11+
| `int` | `Int32` | No |
12+
| `int64` | `Int64` | No |
13+
| `float32` | `Float32` | No |
14+
| F# record | Class with `__slots__` | No |
15+
| F# union | Class with `tag`, `fields` | No |
16+
| F# array | `FSharpArray` | No |
17+
| `ResizeArray<T>` | `list` | Yes |
18+
| `nativeint` | `int` | Yes |
19+
| `string` | `str` | Yes |
20+
21+
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.
22+
23+
### Why Can't Fable's Int32 Just Inherit from Python's int?
24+
25+
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.
26+
27+
## The Solution: fableDefault
28+
29+
The `Fable.Python.Json` module provides a `fableDefault` function that handles Fable types:
30+
31+
```fsharp
32+
open Fable.Python.Json
33+
34+
// Use the convenience function (recommended)
35+
let jsonStr = dumps myObject
36+
37+
// Or use json.dumps with fableDefault explicitly
38+
let jsonStr = json.dumps(myObject, ``default`` = fableDefault)
39+
40+
// With indentation
41+
let prettyJson = dumpsIndented myObject 2
42+
```
43+
44+
### What fableDefault Handles
45+
46+
| Type | Serialization |
47+
| ------------------------------------- | ------------------------------------------- |
48+
| `Int8`, `Int16`, `Int32`, `Int64` | → Python `int` |
49+
| `UInt8`, `UInt16`, `UInt32`, `UInt64` | → Python `int` |
50+
| `Float32`, `Float64` | → Python `float` |
51+
| F# Records (with `__slots__`) | → Python `dict` |
52+
| F# Unions (with `tag`, `fields`) |`["CaseName", ...fields]` or `"CaseName"` |
53+
54+
## Usage Examples
55+
56+
### Basic Serialization
57+
58+
```fsharp
59+
open Fable.Python.Json
60+
61+
// Anonymous record with F# int (compiles to Int32)
62+
let data = {| id = 42; name = "Alice" |}
63+
let json = dumps data
64+
// Output: {"id": 42, "name": "Alice"}
65+
66+
// F# record
67+
type User = { Id: int; Name: string }
68+
let user = { Id = 1; Name = "Bob" }
69+
let json = dumps user
70+
// Output: {"Id": 1, "Name": "Bob"}
71+
72+
// F# discriminated union
73+
type Status = Active | Inactive | Pending of string
74+
let status = Pending "review"
75+
let json = dumps status
76+
// Output: ["Pending", "review"]
77+
```
78+
79+
### With Web Frameworks
80+
81+
#### Flask
82+
83+
Flask's `jsonify` does **not** handle Fable types. Use `dumps` from `Fable.Python.Json`:
84+
85+
```fsharp
86+
open Fable.Python.Flask
87+
open Fable.Python.Json
88+
89+
[<APIClass>]
90+
type Routes() =
91+
[<Get("/users/<int:user_id>")>]
92+
static member get_user(user_id: int) : string =
93+
// Use dumps for Fable type support
94+
dumps {| id = user_id; name = "Alice" |}
95+
96+
[<Get("/simple")>]
97+
static member simple() : obj =
98+
// jsonify works ONLY with native Python types
99+
jsonify {| message = "Hello"; count = 42n |} // 'n' suffix = native int
100+
```
101+
102+
#### FastAPI
103+
104+
FastAPI's `jsonable_encoder` does **not** handle Fable types in anonymous records. You have two options:
105+
106+
**Option 1: Use Pydantic models** (recommended for FastAPI)
107+
108+
```fsharp
109+
open Fable.Python.FastAPI
110+
open Fable.Python.Pydantic
111+
112+
[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>]
113+
type UserResponse(Id: int, Name: string) =
114+
inherit BaseModel()
115+
member val Id: int = Id with get, set
116+
member val Name: string = Name with get, set
117+
118+
[<APIClass>]
119+
type API() =
120+
[<Get("/users/{user_id}")>]
121+
static member get_user(user_id: int) : UserResponse =
122+
UserResponse(Id = user_id, Name = "Alice") // Works! Pydantic handles Int32
123+
```
124+
125+
**Option 2: Use nativeint for anonymous records**
126+
127+
```fsharp
128+
[<Delete("/items/{item_id}")>]
129+
static member delete_item(item_id: int) : obj =
130+
{| status = "deleted"; id = nativeint item_id |} // Convert to native int
131+
```
132+
133+
### Collections
134+
135+
Use `ResizeArray<T>` instead of F# arrays for web API responses:
136+
137+
```fsharp
138+
// Good - ResizeArray compiles to Python list
139+
let users = ResizeArray<User>()
140+
users.Add(User(Id = 1, Name = "Alice"))
141+
let json = dumps users
142+
143+
// Avoid - F# array compiles to FSharpArray
144+
let users = [| User(Id = 1, Name = "Alice") |] // May not serialize correctly
145+
```
146+
147+
## Quick Reference
148+
149+
| Scenario | Solution |
150+
|----------|----------|
151+
| JSON API with Fable types | Use `Fable.Python.Json.dumps` |
152+
| Flask endpoint | Use `dumps` instead of `jsonify` |
153+
| FastAPI endpoint | Use Pydantic models or `nativeint` |
154+
| Int literals in anonymous records | Use `42n` suffix for native int |
155+
| Collections in API responses | Use `ResizeArray<T>` |
156+
| F# array needed | Convert with `ResizeArray(myArray)` |
157+
158+
## API Reference
159+
160+
```fsharp
161+
module Fable.Python.Json
162+
163+
/// Default serializer for Fable types
164+
val fableDefault: obj -> obj
165+
166+
/// Serialize to JSON with Fable type support
167+
val dumps: obj -> string
168+
169+
/// Serialize to JSON with indentation
170+
val dumpsIndented: obj -> int -> string
171+
172+
/// Serialize to file with Fable type support
173+
val dump: obj -> TextIOWrapper -> unit
174+
175+
/// Serialize to file with indentation
176+
val dumpIndented: obj -> TextIOWrapper -> int -> unit
177+
178+
/// Raw Python json module (use with fableDefault for Fable types)
179+
val json: IExports
180+
```
181+
182+
## Further Reading
183+
184+
- [PyO3 Issue #991](https://github.com/PyO3/pyo3/issues/991) - Why Int32 can't subclass Python's int
185+
- [Python json module](https://docs.python.org/3/library/json.html) - Standard library documentation
186+
- [Pydantic](https://docs.pydantic.dev/) - Data validation for Python (works with Fable's Int32)

0 commit comments

Comments
 (0)