Without Type Validation
If type validation doesn't matter then you could use the extra option allow:
'allow' will assign the attributes to the model.
class JsonData(BaseModel):
    ts: int
    class Config:
        extra = "allow"
This will give:
data = JsonData.parse_raw("""
{
   "ts": 222,
   "f3": "cc",
   "f4": "dd"
}
""")
repr(data)
# "JsonData(ts=222, f4='dd', f3='cc')"
And the fields can be accessed via:
print(data.f3)
# cc
With Type Validation
However, if you could change the request body to contain an object holding the "dynamic fields", like the following:
{
  "ts": 111,
  "fields": {
    "f1": "aa",
    "f2": "bb"
  }
}
or
{
  "ts": 222,
  "fields": {
    "f3": "cc",
    "f4": "dd"
  }
}
you could use a Pydantic model like this one:
from pydantic import BaseModel
class JsonData(BaseModel):
    ts: int
    fields: dict[str, str] = {}
That way, any number of fields could be processed, while the field type would be validated, e.g.:
# No "extra" fields; yields an empty dict:
data = JsonData.parse_raw("""
{
  "ts": "222"
}
""")
repr(data)
# "JsonData(ts=222, fields={})"
# One extra field:
data = JsonData.parse_raw("""
{
  "ts": 111,
  "fields": {
    "f1": "aa"
  }
}
""")
repr(data)
# "JsonData(ts=111, fields={'f1': 'aa'})"
# Several extra fields:
data = JsonData.parse_raw("""
{
  "ts": 222,
  "fields": {
    "f2": "bb",
    "f3": "cc",
    "f4": "dd"
  }
}
""")
repr(data)
# "JsonData(ts=222, fields={'f2': 'bb', 'f3': 'cc', 'f4': 'dd'})"
The fields can be accessed like this:
print(data.fields["f2"])
# bb
In this context, you might also want to consider the field types to be StrictStr as opposed to str. When using str, other types will get coerced into strings, for example when an integer or float is passed in. This doesn't happen with StrictStr.
See also: this workaround.