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 typeSent asBecomes
String, Number, Boolean"Widget", 12the 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 targetClassanythingstored exactly as sent
An Array of pointers needs targetClass Without it there is nothing to build a pointer to, so the array is stored raw — a list of plain objects rather than references. The field appears to save and the relationships are not there. A 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.

Undeclared input is never written If a field is not declared with @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!;
Why declare it rather than overwrite it

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.
The master key removes all of this 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 objectSets className on the input so Parse can build from it. Harmless for req.params, surprising if you reuse the same object
Absent means untouchedA declared field not present in the body is not set, which is what makes partial updates work
Query results still need a castfromParams returns your typed class; Parse.Query does not

In short