Guide
fromParams
Turns a request body into a typed Parse object, reading your
@ParseField declarations to convert pointers, dates and
geopoints. It is designed to be pointed straight at raw user input.
The alternative
Without it, every create and update is a run of set() calls that drift from the model each time a field is added:
// Twelve lines that must be edited every time the model changes,
// in both createProduct and updateProduct.
const product = new Product();
product.set('name', req.params.name);
product.set('price', req.params.price);
product.set('releasedAt', new Date(req.params.releasedAt));
const Category = Parse.Object.extend('Category');
const category = new Category();
category.id = req.params.category.objectId;
product.set('category', category);
// … and so on
// The same thing, and it stays correct when the model changes.
const product = Product.fromParams(req.params);
What it converts
It walks your declared fields and converts each according to its type.
| Declared type | Sent as | Becomes |
|---|---|---|
String, Number, Boolean | "Widget", 12 | the value, unchanged |
Date | "2026-03-01T00:00:00Z" | a real Date |
Object | {"colour":"red"} | passed through untouched |
GeoPoint | {latitude, longitude} | a Parse.GeoPoint |
Pointer | {"objectId":"abc"} or {"id":"abc"} | a pointer to targetClass |
Array with targetClass | [{"objectId":"t1"}, …] | an array of pointers |
Array without targetClass | anything | stored exactly as sent |
Pointer without targetClass
throws at import, but an Array cannot, because a plain array is legitimate.
Clearing a pointer
Both null and {} mean remove this relationship, so a form that submits an empty select works without special handling:
{ "category": null } // cleared
{ "category": {} } // cleared
A field that is simply absent from the body is left alone — which is what makes a partial update work.
File and image fields are skipped
Pointer fields whose target is an excluded class — IMG and
File by default — are deliberately not converted. An uploaded file has to
be saved and processed before it can be attached, so a pointer built from raw params
would reference nothing. Nothing is written for those fields; handle them explicitly.
See Files & images. The list is
configurable through
configureKit.
One call for create and update
// No id in the body -> insert
Product.fromParams({ name: 'New' });
// id present -> the same call updates that row
Product.fromParams({ id: 'prod789', name: 'Edited' });
That is why createProduct and updateProduct can share a body.
It also has a consequence worth understanding — see
the CLP note below.
Why it is safe on raw input
The obvious worry about mapping a request body onto a model is over-posting: a caller
adding {"isAdmin": true} and having it stored. That does not happen, and
the reason is worth knowing because it is not obvious from the code.
Product.fromParams({ name: 'ok', isAdmin: true, role: 'root' });
// present on the object: ["name", "isAdmin", "role"]
// sent to the database: { "name": "ok" }
Parse.Object.fromJSON seeds the object with the whole payload, but marks
those values clean — as though they had been fetched from the database. Only the
fields the conversion loop explicitly sets become dirty, and Parse sends dirty
fields only.
@ParseField, no caller can store it through
this path — whatever they send. That is asserted by the test suite against the real save
payload, not merely assumed.
One consequence to keep in mind: undeclared values are readable in memory. A
@BeforeSave doing req.object.get('isAdmin') would see caller
input even though it is never stored. Read only fields you declared.
What you do still control
Two things are worth being deliberate about. Neither is a flaw — both are the natural consequence of mapping a body onto a model.
Server-controlled fields
If a field is declared, a caller can set it. A status,
isApproved or ownerId that your server means to own will be
taken from the body if it is there — with no error, and no log. That is mass
assignment, and it is the reason clientWritable exists.
Declare it on the model, and fromParams discards the field whatever
the request contains:
@ParseField({type: 'String', enum: ['pending', 'approved'], clientWritable: false})
declare status: string;
@ParseField({type: 'Pointer', targetClass: '_User', clientWritable: false})
declare owner: Parse.User;
It governs fromParams only, not the field. Your own code writes it as
freely as ever:
const product = Product.fromParams(req.params);
// Ignored if the body sent them; yours to set here.
product.status = 'pending';
product.owner = req.user!;
Overwriting after the call works, and it is still correct. But it has to be done in
every endpoint that builds this model, and remembered by everyone who adds the next
one. Forgetting is silent — the value is simply the caller's. Declared on the field,
the rule holds wherever fromParams is used.
This is the mirror of Parse's protectedFields, which hides fields on the
way out. clientWritable refuses them on the way in.
A @BeforeSave trigger is the stronger place for this, because it applies on
every save path rather than only this endpoint:
import {
BeforeSave, validateOrThrow, roleKey, implementACL, syncImageAcl,
CloudFunction, catchError,
} from 'parse-server-kit';
@BeforeSave()
static async onBeforeSave(req: Parse.Cloud.BeforeSaveRequest<Product>) {
const product = req.object as Product;
// A new row always starts pending, whoever asked.
if (!product.existed()) product.status = 'pending';
validateOrThrow(product);
}
An id changes which permission applies
Because an id in the body turns the save into an update, Parse checks the
update permission rather than create — the row's ACL write access, and the
class's update CLP.
That protection is real. With a session token, a caller can only update rows they were already allowed to update; sending someone else's id gains them nothing. There is no guard to add here, because the platform already applies one.
It is worth one check, though — that your update rule is not looser than your create rule:
clp: {
create: { [roleKey('Admin')]: true }, // strict
update: { requiresAuthentication: true }, // looser
}
// A body carrying an id reaches the looser rule, so "create is
// admin-only" is not the whole story for that endpoint.
save(null, {useMasterKey: true}) bypasses CLP and ACL entirely, so a create
endpoint built that way can be turned into an update of any row by a body
carrying an id.
That is not specific to
fromParams — passing user input to anything with
the master key has the same effect. Use {sessionToken} for request
handling, and keep the master key for migrations, seeds and cron jobs. See
Permissions & ACL.
An ACL sent in the body
A caller can include ACL in the payload. It is not saved — like any
undeclared value it stays clean — but getACL() will return it until you set
your own.
That only matters in one sequence: calling syncImageAcl(parent, [...])
before setting the parent's ACL would copy the caller's ACL onto the images,
and those are saved. Setting the ACL first, as
documented, removes the question entirely.
product.setACL(implementACL({ … })); // yours, first
syncImageAcl(product, ['cover']); // then cascade
await product.save(null, { sessionToken });
A complete create endpoint
@CloudFunction({
methods: ['POST'],
validation: { requireUser: true, fields: { name: { required: true } } },
})
static async createProduct(req: Parse.Cloud.FunctionRequest) {
const user = req.user!;
// Converts pointers, dates and geopoints from the declared fields.
const product = Product.fromParams(req.params);
// Anything the caller must not decide.
product.set('owner', user);
product.set('status', 'pending');
// Who may see and change it.
product.setACL(implementACL({
owner: [{ user: user.id, read: true, write: true }],
roleRules: [{ role: 'Admin', read: true, write: true }],
}));
// The session token, not the master key - so ACL and CLP apply.
const [err, saved] = await catchError(
product.save(null, { sessionToken: user.getSessionToken() })
);
if (err) throw err;
return saved;
}
Smaller things
| It mutates the params object | Sets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object |
| Absent means untouched | A declared field not present in the body is not set, which is what makes partial updates work |
| Query results still need a cast | fromParams returns your typed class; Parse.Query does not |
In short
- Converts pointers, arrays of pointers, dates and geopoints from your declarations.
- Never writes a field you did not declare, whatever the caller sends.
- Skips file and image pointers, which need handling of their own.
- An
idin the body makes it an update — and the ACL andupdateCLP apply to that, so it is protected. - Overwrite server-controlled fields afterwards, or in a
@BeforeSave. - Save with a session token; keep the master key for administrative work.