Programação Por Equipe DevBras

Capa do artigo: Campo que só aceita números: defina o número e confira no servidor

Campo que só aceita números: defina o número e confira no servidor

Quantidade, código com zeros, CEP e preço são "números" diferentes. Escolha o input certo, escreva a regra de cada um e valide no servidor sem cair nas armadilhas do $ e do \d.

Na tela de entrada de estoque de uma loja de autopeças, o campo de quantidade bloqueia letras no teclado. Mesmo assim, o banco tem um registro com quantidade "12a" convertida em 12, e um código de peça "00012345" que virou 12345 e não casa mais com o fornecedor. A resposta curta: "só números" não é uma regra, são várias. Defina o que é número em cada campo (inteiro com faixa, código de tamanho fixo que continua texto, decimal com vírgula), use no navegador um input que ajude a digitar e valide de novo no servidor, recusando o valor com uma mensagem em vez de "limpar" o que veio. E, se usar expressão regular, cuidado com o $ e o \d, que não significam a mesma coisa em todas as linguagens.

O bloqueio de teclado e a máscara são tratados no guia a máscara não é a validação. Aqui o foco é a regra: o que cada campo aceita e como conferir isso do jeito certo.

Por que "só números" engana

O teclado não é a única porta de entrada. A pessoa cola um texto, o gerenciador de formulários preenche sozinho, um script integra com a API, alguém monta a requisição na mão. Tudo o que o navegador faz é conforto; o servidor recebe texto e precisa decidir.

O segundo engano é tratar todos os campos numéricos como iguais. Na loja, são quatro casos:

  • Quantidade: inteiro de 1 a 9999. É um número de verdade, que se soma.
  • Código da peça: exatamente 8 algarismos, com zeros à esquerda. Parece número, mas é um identificador. Guardado como inteiro, perde os zeros.
  • CEP: 8 algarismos. A pessoa digita "01310-100"; o hífen é um separador conhecido e pode ser removido de propósito.
  • Preço: decimal com vírgula e duas casas. Uma regra de "só algarismos" recusaria um preço válido.

Como definir e validar cada campo

  1. Escreva a regra de cada campo numa frase. "Quantidade: inteiro de 1 a 9999, só algarismos." Essa frase vira a mensagem de erro.
  2. Escolha o tipo de armazenamento. Quantidade é inteiro; código e CEP são texto; preço vira centavos em inteiro, como em dinheiro em centavos.
  3. No navegador, use input de texto com teclado numérico para códigos e quantidades.
  4. Normalize só o que é separador conhecido, como o hífen do CEP. Nunca remova letras em silêncio.
  5. Valide no servidor com a mesma regra e responda com o nome do campo e a mensagem.
  6. Teste a API diretamente, sem passar pela tela.

O input certo no navegador

O type="number" parece a escolha óbvia, mas foi pensado para quantidades que se ajustam com setas. Ele aceita caracteres como e, + e - (porque 1e3 é um número válido), em alguns navegadores muda o valor quando a pessoa rola a página com o campo em foco, e o seu valueAsNumber transforma "00012345" em 12345. Para código, CEP e quantidade simples, o arranjo mais previsível é texto com teclado numérico:

<label for="codigo">Código da peça</label>
<input id="codigo" name="codigo" type="text" inputmode="numeric"
       autocomplete="off" maxlength="8" aria-describedby="codigo-ajuda">
<p id="codigo-ajuda">8 algarismos, como aparece na nota do fornecedor. Ex.: 00012345</p>

<label for="quantidade">Quantidade</label>
<input id="quantidade" name="quantidade" type="text" inputmode="numeric"
       autocomplete="off" aria-describedby="quantidade-ajuda">
<p id="quantidade-ajuda">De 1 a 9999.</p>

O inputmode="numeric" abre o teclado de algarismos no celular, sem impor as regras do type="number". A ajuda com exemplo fica visível, ligada ao campo por aria-describedby.

As regras em JavaScript

As mesmas funções podem rodar no navegador, para dar retorno rápido, e no servidor Node, onde a decisão vale:

function lerQuantidade(bruto) {
  const texto = String(bruto ?? '').trim()
  if (!/^[0-9]{1,4}$/.test(texto)) {
    return { erro: 'Quantidade: use só algarismos, de 1 a 9999.' }
  }
  const valor = Number(texto)
  if (valor < 1) return { erro: 'Quantidade: o mínimo é 1.' }
  return { valor }
}

function lerCodigoPeca(bruto) {
  const texto = String(bruto ?? '').trim()
  if (!/^[0-9]{8}$/.test(texto)) {
    return { erro: 'Código da peça: são exatamente 8 algarismos. Ex.: 00012345.' }
  }
  return { valor: texto }
}

function lerCep(bruto) {
  const texto = String(bruto ?? '').trim().replace(/^([0-9]{5})-([0-9]{3})$/, '$1$2')
  if (!/^[0-9]{8}$/.test(texto)) {
    return { erro: 'CEP: informe 8 algarismos, com ou sem hífen. Ex.: 01310-100.' }
  }
  return { valor: texto }
}

function lerPrecoEmCentavos(bruto) {
  const m = /^([0-9]{1,7}),([0-9]{2})$/.exec(String(bruto ?? '').trim())
  if (!m) return { erro: 'Preço: use vírgula e duas casas. Ex.: 189,90.' }
  return { valor: Number(m[1]) * 100 + Number(m[2]) }
}

Repare que o código da peça volta como texto, com os zeros. O CEP só tem o hífen removido quando ele está exatamente na posição esperada; "0131a-100" continua sendo recusado. E o preço aceita só o formato que a tela pede. Se você quiser aceitar "1.234,50", acrescente isso à regra de propósito, com teste, em vez de apagar pontos no escuro.

As armadilhas do $ e do \d

A expressão ^\d+$ é a resposta mais copiada para "só números", e ela se comporta diferente conforme a linguagem.

Em JavaScript, sem a flag m, o $ casa só no fim do texto e o \d equivale a [0-9]. Lá, ela faz o que promete. Em PHP (PCRE) e em .NET, o $ também casa antes de uma quebra de linha final. Ou seja, "123\n" passa. Em .NET, além disso, o \d aceita algarismos de outros sistemas de escrita, como os arábico-índicos:

Regex.IsMatch("123\n", @"^\d+$");      // true: o $ aceitou a quebra de linha
Regex.IsMatch("١٢٣", @"^\d+$");        // true: \d aceita algarismos Unicode
Regex.IsMatch("123\n", @"^[0-9]+\z");  // false: [0-9] e \z fecham as duas portas

A correção é usar [0-9] no lugar de \d e \z no lugar de $ para o fim absoluto do texto. Em PHP, \z também funciona, assim como o modificador D. No C#, para inteiros, você pode dispensar a regex e confiar no parser com estilo estrito:

using System.Globalization;

public static bool TentarLerQuantidade(string? bruto, out int quantidade)
{
    var texto = (bruto ?? "").Trim();
    return int.TryParse(texto, NumberStyles.None, CultureInfo.InvariantCulture, out quantidade)
        && quantidade is >= 1 and <= 9999;
}

Com NumberStyles.None, o TryParse recusa sinal, espaço interno, separador de milhar e ponto decimal. O Trim antes dele é uma decisão explícita: espaço nas pontas é tolerado; qualquer outra coisa, não.

A resposta do servidor

Quando a regra falha, o servidor recusa a requisição inteira, com status 422 e a mensagem por campo, a mesma frase que a tela usa (o guia mensagem de erro igual na API mostra como manter uma fonte só). O teste que importa é o que passa por fora da tela:

curl -i -X POST https://loja.exemplo.com.br/api/estoque/entradas \
  -H "Content-Type: application/json" \
  -d '{"codigo":"00012345","quantidade":"12a"}'

A resposta esperada:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{"erros":{"quantidade":"Quantidade: use só algarismos, de 1 a 9999."}}

Se a API devolver 200 e gravar 12, ela está decidindo pela pessoa. "12a" pode ser um 12 com a tecla vizinha apertada, ou um 124 em que o 4 virou "a". Só quem digitou sabe.

Erros comuns

  • Validar só no navegador. Qualquer chamada direta à API grava o que quiser.
  • Remover letras em silêncio e gravar o resto. "12a" vira 12 e ninguém fica sabendo que o valor mudou.
  • Guardar código com zeros à esquerda como inteiro. "00012345" vira 12345 e para de casar com o sistema do fornecedor.
  • Usar ^\d+$ em .NET ou PHP. Quebra de linha final e algarismos de outros alfabetos passam pela validação.
  • Usar a regra de inteiro para preço. A vírgula é recusada ou removida, e 189,90 vira 18990 reais.
  • Usar type="number" para código. A rolagem do mouse altera o valor e o zero à esquerda some.

Checklist

  • Cada campo numérico tem uma frase que diz o que ele aceita.
  • Códigos, CEP e documentos são guardados como texto.
  • Valores monetários são convertidos para centavos com regra explícita.
  • Os inputs de código e quantidade usam texto com inputmode="numeric".
  • O servidor valida com a mesma regra e responde 422 com o campo.
  • Expressões regulares usam [0-9] e fim absoluto quando a linguagem exige.
  • Existe um teste que envia "12a" direto à API e espera recusa.

Envie "12a" e "123" seguido de quebra de linha direto para a API do seu formulário mais usado. Ela recusa os dois, ou algum deles foi gravado?

Avalie este artigo

Referência: https://stackoverflow.com/questions/19715303/regex-that-accepts-only-numbers-0-9-and-no-characters